@craftedstudios/avatars 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +126 -2
- package/package.json +30 -4
- package/src/avatar.js +55 -0
- package/src/color.js +234 -0
- package/src/cursor.js +137 -0
- package/src/element.js +37 -0
- package/src/goo.js +70 -0
- package/src/index.js +7 -0
- package/src/names.js +44 -0
- package/src/options.js +88 -0
- package/src/react.js +133 -0
- package/src/renderer.js +403 -0
- package/src/seed.js +67 -0
- package/src/shader.js +298 -0
- package/src/still.js +60 -0
- package/types/index.d.ts +114 -0
- package/types/react.d.ts +27 -0
package/src/shader.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// One avatar per draw. The renderer points the viewport at a tile of the shared
|
|
2
|
+
// canvas and this shader fills it: the shape (with a drop drawn out of it when
|
|
3
|
+
// it leans), the effect dragged along into the drop, film grain, and an
|
|
4
|
+
// optional ring.
|
|
5
|
+
|
|
6
|
+
export const VERT = `attribute vec2 p; void main() { gl_Position = vec4(p, 0.0, 1.0); }`;
|
|
7
|
+
|
|
8
|
+
export const FRAG = `
|
|
9
|
+
precision highp float;
|
|
10
|
+
uniform vec4 uTile; // tile origin (GL, y up) and size, device px
|
|
11
|
+
uniform vec2 uC, uE; // center and the drop's center (tile px, y down)
|
|
12
|
+
uniform float uR; // radius, device px
|
|
13
|
+
uniform float uDropR; // the drop's radius, device px
|
|
14
|
+
uniform float uGoo; // how far apart body and drop still melt together, in r
|
|
15
|
+
uniform float uBulge; // how much of the pull the body itself follows
|
|
16
|
+
uniform float uShape; // 0 circle, 1 squircle, 2 square
|
|
17
|
+
uniform float uRing; // ring width, device px
|
|
18
|
+
uniform vec3 uRingColor;
|
|
19
|
+
uniform float uIn; // entrance, 0..1
|
|
20
|
+
uniform float uFx; // which effect; see FX in renderer.js
|
|
21
|
+
uniform vec3 uCol1, uCol2, uCol3, uCol4; // the effect's colors, in its own roles
|
|
22
|
+
uniform float uT; // seconds, phase included
|
|
23
|
+
uniform float uGrain, uGrainCell;
|
|
24
|
+
uniform float uAngle; // per-seed angle for effects that have a direction
|
|
25
|
+
uniform vec4 uLayout; // per-seed pattern offset (xy), turn (z) and zoom (w)
|
|
26
|
+
uniform float uWarp; // distortion: 1 is each effect's own amount
|
|
27
|
+
uniform float uScale; // pattern zoom: above 1 makes features larger
|
|
28
|
+
uniform vec2 uMouse; // the pointer, smoothed (tile px, y down)
|
|
29
|
+
uniform float uMouseAct; // how much the pointer is acting, 0..1, eased in and out
|
|
30
|
+
uniform float uMode; // 1 follow, 2 repel, 3 distort, 4 pulse, 5 orbit, 6 turbulence
|
|
31
|
+
|
|
32
|
+
float hash(vec2 p) { return fract(sin(dot(p, vec2(41.3, 289.1))) * 43758.5); }
|
|
33
|
+
float noise(vec2 p) {
|
|
34
|
+
vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
|
35
|
+
return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Film grain: a sinless hash (no diagonal banding), re-rolled each frame and
|
|
39
|
+
// strongest in the midtones. The cell grows with the render so large stills
|
|
40
|
+
// keep visible grain instead of per-pixel dust.
|
|
41
|
+
float rfHash12(vec2 p) {
|
|
42
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
43
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
44
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
45
|
+
}
|
|
46
|
+
float rfFilmGrain(vec2 px, vec3 col) {
|
|
47
|
+
float t = floor(fract(uT * 0.731) * 240.0);
|
|
48
|
+
float n = rfHash12(floor(px / uGrainCell) + vec2(t * 17.0, t * 9.0));
|
|
49
|
+
float luma = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
|
50
|
+
float resp = mix(0.35, 1.0, smoothstep(0.0, 0.22, luma)) * (1.0 - 0.8 * smoothstep(0.72, 1.0, luma));
|
|
51
|
+
return (n - 0.5) * resp * 1.5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// OKLab, a color space where a straight line between two colors looks even.
|
|
55
|
+
vec3 toLab(vec3 c) {
|
|
56
|
+
c = pow(max(c, 0.0), vec3(2.2));
|
|
57
|
+
vec3 lms = pow(vec3(
|
|
58
|
+
0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b,
|
|
59
|
+
0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b,
|
|
60
|
+
0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b), vec3(1.0 / 3.0));
|
|
61
|
+
return vec3(
|
|
62
|
+
0.2104542553 * lms.x + 0.7936177850 * lms.y - 0.0040720468 * lms.z,
|
|
63
|
+
1.9779984951 * lms.x - 2.4285922050 * lms.y + 0.4505937099 * lms.z,
|
|
64
|
+
0.0259040371 * lms.x + 0.7827717662 * lms.y - 0.8086757660 * lms.z);
|
|
65
|
+
}
|
|
66
|
+
vec3 fromLab(vec3 L) {
|
|
67
|
+
vec3 lms = vec3(
|
|
68
|
+
L.x + 0.3963377774 * L.y + 0.2158037573 * L.z,
|
|
69
|
+
L.x - 0.1055613458 * L.y - 0.0638541728 * L.z,
|
|
70
|
+
L.x - 0.0894841775 * L.y - 1.2914855480 * L.z);
|
|
71
|
+
lms = lms * lms * lms;
|
|
72
|
+
vec3 c = vec3(
|
|
73
|
+
4.0767416621 * lms.x - 3.3077115913 * lms.y + 0.2309699292 * lms.z,
|
|
74
|
+
-1.2684380046 * lms.x + 2.6097574011 * lms.y - 0.3413193965 * lms.z,
|
|
75
|
+
-0.0041960863 * lms.x - 0.7034186147 * lms.y + 1.7076147010 * lms.z);
|
|
76
|
+
return pow(clamp(c, 0.0, 1.0), vec3(1.0 / 2.2));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Blends two colors in OKLab, where the halfway point looks halfway: the mix
|
|
80
|
+
// stays as light as its ends rather than dipping dark and dirty the way a
|
|
81
|
+
// straight RGB fade does.
|
|
82
|
+
vec3 blend(vec3 a, vec3 b, float t) {
|
|
83
|
+
return fromLab(mix(toLab(a), toLab(b), t));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Four colors along g (0 to 1), the first covering about half and the rest
|
|
87
|
+
// narrower, each held a moment before it turns into the next.
|
|
88
|
+
vec3 steps4(float g, vec3 c1, vec3 c2, vec3 c3, vec3 c4) {
|
|
89
|
+
if (g < 0.34) return c1;
|
|
90
|
+
if (g < 0.57) return blend(c1, c2, smoothstep(0.34, 0.57, g));
|
|
91
|
+
if (g < 0.59) return c2;
|
|
92
|
+
if (g < 0.76) return blend(c2, c3, smoothstep(0.59, 0.76, g));
|
|
93
|
+
if (g < 0.78) return c3;
|
|
94
|
+
if (g < 0.93) return blend(c3, c4, smoothstep(0.78, 0.93, g));
|
|
95
|
+
return c4;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
vec2 rot2(vec2 v, float a) { float c = cos(a), s = sin(a); return vec2(v.x * c - v.y * s, v.x * s + v.y * c); }
|
|
99
|
+
|
|
100
|
+
// Plasma: interference waves through a light organic distortion, cycled
|
|
101
|
+
// through the four colors.
|
|
102
|
+
vec3 fxPlasma(vec2 uv, float t, vec3 c1, vec3 c2, vec3 c3, vec3 c4) {
|
|
103
|
+
t *= 0.15;
|
|
104
|
+
vec2 p = uv * 1.8;
|
|
105
|
+
for (float i = 1.0; i <= 3.0; i += 1.0) {
|
|
106
|
+
p.x += 0.16 * uWarp / i * sin(i * 1.7 * p.y + t * 0.9 + i * 0.3);
|
|
107
|
+
p.y += 0.16 * uWarp / i * cos(i * 2.1 * p.x + t * 1.1 - i * 0.5);
|
|
108
|
+
}
|
|
109
|
+
float v = sin(p.x * 3.0 + t) + sin(p.y * 2.7 - t * 0.7) + sin((p.x + p.y) * 2.3 + t * 0.5);
|
|
110
|
+
vec2 c = p - 0.9;
|
|
111
|
+
v += sin(length(c) * 4.0 - t * 1.3);
|
|
112
|
+
v += 0.7 * sin(p.x * 1.8 - p.y * 2.5 + t * 0.8) + 0.5 * sin(rot2(p, 0.7).x * 3.5 + t * 0.4);
|
|
113
|
+
v = v / 5.2 * 0.5 + 0.5;
|
|
114
|
+
float g = fract(v * 1.5);
|
|
115
|
+
// The loop closes back into the first color, which covers about half.
|
|
116
|
+
if (g > 0.88) return blend(c4, c1, smoothstep(0.88, 1.0, g));
|
|
117
|
+
return steps4(g / 0.88, c1, c2, c3, c4);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Linear Gradient: four colors in bands along an angle set by the seed,
|
|
121
|
+
// moved by a light sine warp.
|
|
122
|
+
vec3 fxLinear(vec2 uv, float t, float ang, vec3 c1, vec3 c2, vec3 c3, vec3 c4) {
|
|
123
|
+
t *= 0.15;
|
|
124
|
+
vec2 p = uv;
|
|
125
|
+
for (float i = 1.0; i <= 2.0; i += 1.0) {
|
|
126
|
+
p.x += 0.12 * uWarp / i * sin(i * 5.1 * p.y + t * 0.9 + i * 0.3);
|
|
127
|
+
p.y += 0.12 * uWarp / i * cos(i * 6.3 * p.x + t * 1.1 - i * 0.5);
|
|
128
|
+
}
|
|
129
|
+
float g = clamp((p.x - 0.5) * cos(ang) + (p.y - 0.5) * sin(ang) + 0.5, 0.0, 1.0);
|
|
130
|
+
return steps4(g, c1, c2, c3, c4);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Radial Gradient: concentric bands from the rim in, pushed around by
|
|
134
|
+
// two drifting noise fields and held still at the rim.
|
|
135
|
+
vec3 fxRadial(vec2 uv, float t, vec3 c1, vec3 c2, vec3 c3, vec3 c4) {
|
|
136
|
+
t *= 0.15;
|
|
137
|
+
float g = clamp(length(uv - 0.5) * 2.0 / 1.2, 0.0, 1.0);
|
|
138
|
+
float n = noise(uv * 3.0 + t * 0.4) + noise(uv * 2.5 - t * 0.3 + 50.0) - 1.0;
|
|
139
|
+
// The first color is the outer band, the largest by area.
|
|
140
|
+
return steps4(1.0 - clamp(g + n * 0.15 * uWarp * smoothstep(1.0, 0.6, g), 0.0, 1.0), c1, c2, c3, c4);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Thermal: thick sheets of light folding across the avatar, colored by how
|
|
144
|
+
// bright they are, the way a heat camera colors warmth. Each sheet is a broad
|
|
145
|
+
// band around a slow wave, thicker and thinner along its length, fading softly
|
|
146
|
+
// upward in rays and more quickly below. The first color is the cold ground,
|
|
147
|
+
// and the light climbs through the second and third to the fourth in each
|
|
148
|
+
// sheet's core, held with short blends like the gradients. Upright: it isn't
|
|
149
|
+
// turned per seed.
|
|
150
|
+
vec3 fxThermal(vec2 uv, float t, vec3 c1, vec3 c2, vec3 c3, vec3 c4) {
|
|
151
|
+
t *= 0.15;
|
|
152
|
+
float light = 0.0;
|
|
153
|
+
for (int k = 0; k < 2; k++) {
|
|
154
|
+
float fk = float(k);
|
|
155
|
+
float x = uv.x * (1.0 + fk * 0.4) + fk * 5.3;
|
|
156
|
+
// The main sheet low across the avatar, a fainter one higher up.
|
|
157
|
+
float mid = mix(0.62, 0.34, fk)
|
|
158
|
+
+ (mix(0.12, 0.08, fk) * sin(x * 2.0 + t * (0.8 + fk * 0.3) + fk * 2.0)
|
|
159
|
+
+ 0.04 * sin(x * 4.7 - t * 1.2 + fk)
|
|
160
|
+
+ 0.07 * (noise(vec2(x * 1.4, t * 0.3 + fk * 4.0)) - 0.5)) * uWarp;
|
|
161
|
+
float thick = mix(0.13, 0.09, fk) * (0.75 + 0.5 * noise(vec2(x * 1.8 - t * 0.3, fk * 5.0)));
|
|
162
|
+
float d = mid - uv.y; // above the sheet's middle when positive
|
|
163
|
+
float ray = 0.75 + 0.5 * noise(vec2(x * 9.0 + t * 0.5, fk * 9.0));
|
|
164
|
+
float up = thick * 1.6 * ray, down = thick * 0.7;
|
|
165
|
+
float body = d > 0.0 ? exp(-pow(d / up, 1.6)) : exp(-pow(-d / down, 2.0));
|
|
166
|
+
float fold = 0.8 + 0.2 * noise(vec2(x * 2.2 - t * 0.4, fk * 6.0));
|
|
167
|
+
light = 1.0 - (1.0 - light) * (1.0 - clamp(body * fold * mix(1.0, 0.6, fk), 0.0, 1.0));
|
|
168
|
+
}
|
|
169
|
+
return steps4(light, c1, c2, c3, c4);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Topography: contour lines of a warped fBm elevation over a tinted fill.
|
|
173
|
+
float topoFbm(vec2 p) {
|
|
174
|
+
float v = 0.0, a = 0.5;
|
|
175
|
+
for (int i = 0; i < 6; i++) { p = rot2(p, 0.6); v += a * noise(p); p *= 2.0; a *= 0.5; }
|
|
176
|
+
return v;
|
|
177
|
+
}
|
|
178
|
+
vec3 fxTopo(vec2 uv, float t, vec3 line, vec3 fillC, vec3 ground) {
|
|
179
|
+
t *= 0.15;
|
|
180
|
+
vec2 p = uv * 1.6;
|
|
181
|
+
p += vec2(topoFbm(p + t * 0.3), topoFbm(p + vec2(5.2, 1.3) + t * 0.2)) * 0.3 * uWarp;
|
|
182
|
+
float e = topoFbm(p + t * 0.1), se = e * 6.0;
|
|
183
|
+
float ld = min(fract(se), 1.0 - fract(se));
|
|
184
|
+
float l = 1.0 - smoothstep(0.04, 0.14, ld);
|
|
185
|
+
vec3 fill = mix(ground, fillC, e * 0.6);
|
|
186
|
+
return mix(fill, line, l);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Scenery's mouse interactions, as written in its engine, measured across the
|
|
190
|
+
// avatar: they move the pattern around the pointer and leave the shape alone.
|
|
191
|
+
vec2 tsMod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
192
|
+
vec3 tsMod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
193
|
+
vec3 tsPermute(vec3 x) { return tsMod289(((x * 34.0) + 1.0) * x); }
|
|
194
|
+
float tsNoise(vec2 v) {
|
|
195
|
+
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
|
|
196
|
+
vec2 i = floor(v + dot(v, C.yy));
|
|
197
|
+
vec2 x0 = v - i + dot(i, C.xx);
|
|
198
|
+
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
199
|
+
vec4 x12 = x0.xyxy + C.xxzz;
|
|
200
|
+
x12.xy -= i1;
|
|
201
|
+
i = tsMod289(i);
|
|
202
|
+
vec3 p = tsPermute(tsPermute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
|
203
|
+
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
|
204
|
+
m = m * m; m = m * m;
|
|
205
|
+
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
206
|
+
vec3 h = abs(x) - 0.5;
|
|
207
|
+
vec3 ox = floor(x + 0.5);
|
|
208
|
+
vec3 a0 = x - ox;
|
|
209
|
+
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
|
210
|
+
vec3 g;
|
|
211
|
+
g.x = a0.x * x0.x + h.x * x0.y;
|
|
212
|
+
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
213
|
+
return 130.0 * dot(m, g);
|
|
214
|
+
}
|
|
215
|
+
float tsFbm(vec2 p) {
|
|
216
|
+
float v = 0.0, a = 0.5;
|
|
217
|
+
for (int i = 0; i < 5; i++) { v += tsNoise(p) * a; p *= 2.1; a *= 0.5; }
|
|
218
|
+
return v;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
vec2 mouseInteract(vec2 uv, vec2 mp, float act, float t) {
|
|
222
|
+
vec2 diff = uv - mp;
|
|
223
|
+
float dist = length(diff);
|
|
224
|
+
vec2 dir = normalize(diff + 0.0001);
|
|
225
|
+
if (uMode < 1.5) {
|
|
226
|
+
uv -= dir * smoothstep(0.5, 0.0, dist) * 0.08 * act; // follow
|
|
227
|
+
} else if (uMode < 2.5) {
|
|
228
|
+
uv += dir * smoothstep(0.4, 0.0, dist) * 0.06 * act; // repel
|
|
229
|
+
} else if (uMode < 3.5) {
|
|
230
|
+
float inf = smoothstep(0.5, 0.0, dist); // distort
|
|
231
|
+
uv += inf * 0.05 * act * vec2(sin(dist * 10.0 + t * 2.0), cos(dist * 8.0 - t * 1.5));
|
|
232
|
+
} else if (uMode < 4.5) {
|
|
233
|
+
float pulse = sin(dist * 15.0 - t * 4.0) * 0.5 + 0.5; // pulse
|
|
234
|
+
uv += dir * smoothstep(0.5, 0.0, dist) * pulse * 0.04 * act;
|
|
235
|
+
} else if (uMode < 5.5) {
|
|
236
|
+
float inf = smoothstep(0.6, 0.0, dist); // orbit
|
|
237
|
+
uv = mp + rot2(uv - mp, inf * 0.4 * act);
|
|
238
|
+
} else {
|
|
239
|
+
float falloff = exp(-pow(dist / 0.26, 2.0)); // turbulence
|
|
240
|
+
if (falloff > 0.002) {
|
|
241
|
+
float ts = t * 0.32;
|
|
242
|
+
vec2 field = vec2(tsFbm((uv + vec2(0.0, ts * 0.25)) * 5.2), tsFbm((uv + vec2(9.1, -ts * 0.3)) * 5.2));
|
|
243
|
+
uv += (field - 0.5) * 2.0 * falloff * 0.85 * 0.035 * act;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return uv;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Signed distance to the shape, device px. The squircle is a rounded square
|
|
250
|
+
// with corners of 0.6r, which is what border-radius: 30% draws, so the CSS
|
|
251
|
+
// fallback and the live avatar have the same outline.
|
|
252
|
+
float sdShape(vec2 p, float r) {
|
|
253
|
+
if (uShape < 0.5) return length(p) - r;
|
|
254
|
+
float k = uShape < 1.5 ? 0.6 * r : 0.0;
|
|
255
|
+
vec2 q = abs(p) - vec2(r - k);
|
|
256
|
+
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - k;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Polynomial smooth minimum: two shapes closer than k melt into one surface
|
|
260
|
+
// with a neck between them that thins as they part.
|
|
261
|
+
float smin(float a, float b, float k) {
|
|
262
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
263
|
+
return mix(b, a, h) - k * h * (1.0 - h);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
void main() {
|
|
267
|
+
vec2 px = gl_FragCoord.xy - uTile.xy;
|
|
268
|
+
px.y = uTile.w - px.y;
|
|
269
|
+
float r = uR * (0.55 + 0.45 * uIn);
|
|
270
|
+
// The lean is goo: the body bulges a little toward the pointer, and a drop
|
|
271
|
+
// of the same material, drawn out on a spring, melts back into it.
|
|
272
|
+
vec2 pull = uE - uC;
|
|
273
|
+
vec2 body = uC + pull * uBulge;
|
|
274
|
+
float dBody = sdShape(px - body, r);
|
|
275
|
+
float dDrop = length(px - uE) - uDropR * (0.55 + 0.45 * uIn);
|
|
276
|
+
float d = smin(dBody, dDrop, uGoo * r);
|
|
277
|
+
float cover = clamp(0.5 - d / 1.2, 0.0, 1.0) * uIn;
|
|
278
|
+
if (cover <= 0.0) { gl_FragColor = vec4(0.0); return; }
|
|
279
|
+
// The material goes with it: the nearer a point is to the drop, the further
|
|
280
|
+
// back toward the center it samples, so the pattern draws out like taffy.
|
|
281
|
+
float w = clamp(0.5 + 0.5 * (dBody - dDrop) / (uGoo * r), 0.0, 1.0);
|
|
282
|
+
vec2 uv = (px - uC - pull * mix(uBulge, 1.0, w)) / (r * 2.0 * uScale) + 0.5;
|
|
283
|
+
if (uMouseAct > 0.001) uv = mouseInteract(uv, (uMouse - uC) / (r * 2.0 * uScale) + 0.5, uMouseAct, uT);
|
|
284
|
+
// Each seed sees its own part of the pattern. Thermal only slides sideways
|
|
285
|
+
// and zooms, so it stays upright.
|
|
286
|
+
vec2 ua = (uv - 0.5) / uLayout.w + 0.5 + vec2(uLayout.x, 0.0);
|
|
287
|
+
uv = rot2(uv - 0.5, uLayout.z) / uLayout.w + 0.5 + uLayout.xy;
|
|
288
|
+
vec3 c;
|
|
289
|
+
if (uFx < 0.5) c = fxLinear(uv, uT, uAngle, uCol1, uCol2, uCol3, uCol4);
|
|
290
|
+
else if (uFx < 1.5) c = fxThermal(ua, uT, uCol1, uCol2, uCol3, uCol4);
|
|
291
|
+
else if (uFx < 2.5) c = fxTopo(uv, uT, uCol1, uCol2, uCol3);
|
|
292
|
+
else if (uFx < 3.5) c = fxPlasma(uv, uT, uCol1, uCol2, uCol3, uCol4);
|
|
293
|
+
else c = fxRadial(uv, uT, uCol1, uCol2, uCol3, uCol4);
|
|
294
|
+
c += rfFilmGrain(px, c) * uGrain;
|
|
295
|
+
if (uRing > 0.0) c = mix(c, uRingColor, smoothstep(-uRing - 0.8, -uRing + 0.4, d));
|
|
296
|
+
c = clamp(c, 0.0, 1.0);
|
|
297
|
+
gl_FragColor = vec4(c * cover, cover);
|
|
298
|
+
}`;
|
package/src/still.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolve } from './options.js';
|
|
2
|
+
import { paint2d, shaderColors } from './color.js';
|
|
3
|
+
import { getRenderer, parseColor, angleOf } from './renderer.js';
|
|
4
|
+
import { dropRadius, params as gooParams } from './goo.js';
|
|
5
|
+
|
|
6
|
+
const MAX = 2048;
|
|
7
|
+
|
|
8
|
+
// One frame at the seed's phase, for email, OG images and long lists. The image
|
|
9
|
+
// is exactly `size` pixels square and transparent outside the shape.
|
|
10
|
+
// const png = await toDataURL('dexter@craftedstudios.co', { size: 1024 });
|
|
11
|
+
export async function toDataURL(seed, options = {}) {
|
|
12
|
+
return (await still(seed, options)).toDataURL('image/png');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function toBlob(seed, options = {}) {
|
|
16
|
+
const canvas = await still(seed, options);
|
|
17
|
+
return new Promise((ok, fail) => canvas.toBlob((b) => (b ? ok(b) : fail(new Error('Could not encode the avatar'))), 'image/png'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function still(seed, options = {}) {
|
|
21
|
+
if (typeof document === 'undefined') throw new Error('Still images need a browser');
|
|
22
|
+
const size = Math.round(Math.min(MAX, Math.max(1, Number(options.size) || 512)));
|
|
23
|
+
const o = resolve({ ...options, seed, size, interaction: 'none', motion: 'still' });
|
|
24
|
+
const out = document.createElement('canvas');
|
|
25
|
+
out.width = out.height = size;
|
|
26
|
+
const ctx = out.getContext('2d');
|
|
27
|
+
const r = getRenderer();
|
|
28
|
+
if (r && !r.lost) {
|
|
29
|
+
const half = size / 2;
|
|
30
|
+
const g = gooParams(o.stretch, o.thickness);
|
|
31
|
+
const target = {
|
|
32
|
+
dev: size,
|
|
33
|
+
params: () => ({
|
|
34
|
+
cx: half, cy: half, ex: half, ey: half, r: half,
|
|
35
|
+
dropR: dropRadius({ x: 0, y: 0 }, half, g),
|
|
36
|
+
goo: g.goo, bulge: g.bulge, warp: o.distortion * 2, scale: o.scale,
|
|
37
|
+
mx: half, my: half, mouseAct: 0, mode: 0,
|
|
38
|
+
shape: o.shape, ring: o.ring, ringColor: parseColor(o.ringColor),
|
|
39
|
+
enter: 1, effect: o.traits.effect, colors: shaderColors(o.palette),
|
|
40
|
+
t: o.traits.phase, grain: o.grain, angle: angleOf(o.traits), layout: o.traits.layout,
|
|
41
|
+
}),
|
|
42
|
+
blit: (src, x, y, s) => { ctx.globalCompositeOperation = 'copy'; ctx.drawImage(src, x, y, s, s, 0, 0, size, size); },
|
|
43
|
+
};
|
|
44
|
+
r.render([target], 0);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
// Without WebGL: the same gradient the CSS fallback shows, cut to the shape.
|
|
48
|
+
ctx.beginPath();
|
|
49
|
+
if (o.shape === 'circle') ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
|
50
|
+
else if (ctx.roundRect) ctx.roundRect(0, 0, size, size, o.shape === 'squircle' ? size * 0.3 : 0);
|
|
51
|
+
else ctx.rect(0, 0, size, size);
|
|
52
|
+
ctx.clip();
|
|
53
|
+
paint2d(ctx, o.traits.effect, o.palette, size);
|
|
54
|
+
if (o.ring > 0) {
|
|
55
|
+
ctx.lineWidth = o.ring * 2;
|
|
56
|
+
ctx.strokeStyle = o.ringColor;
|
|
57
|
+
ctx.stroke();
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type Effect = 'linear' | 'thermal' | 'topography' | 'plasma' | 'radial';
|
|
2
|
+
export type Shape = 'circle' | 'squircle' | 'square';
|
|
3
|
+
|
|
4
|
+
export interface AvatarOptions {
|
|
5
|
+
/** Any string. Used exactly as given; pass a stable ID. */
|
|
6
|
+
seed: string;
|
|
7
|
+
/** CSS pixels. Rendered at device pixel ratio, capped at 2. Default 32. */
|
|
8
|
+
size?: number;
|
|
9
|
+
/** Default 'auto', which picks from the seed. */
|
|
10
|
+
effect?: Effect | 'auto';
|
|
11
|
+
/**
|
|
12
|
+
* Up to four CSS colors (hex, rgb() or hsl()), used exactly as given in
|
|
13
|
+
* place of the seed's colors: main color, two neighbors, then the dark
|
|
14
|
+
* ground. Any left out are built from the main color. Default: colors from
|
|
15
|
+
* the seed.
|
|
16
|
+
*/
|
|
17
|
+
colors?: string[];
|
|
18
|
+
/** 'still' freezes it. Reduced motion always freezes. Default 'auto'. */
|
|
19
|
+
motion?: 'auto' | 'still';
|
|
20
|
+
/**
|
|
21
|
+
* How the avatar reacts to the pointer (mouse only). 'goo' stretches the
|
|
22
|
+
* outline toward it; the rest move the pattern inside the outline. Reduced
|
|
23
|
+
* motion turns it off. Default 'goo'.
|
|
24
|
+
*/
|
|
25
|
+
interaction?: 'goo' | 'follow' | 'repel' | 'distort' | 'pulse' | 'orbit' | 'turbulence' | 'none';
|
|
26
|
+
/** Film grain strength, 0 to turn it off. Default 0.07. */
|
|
27
|
+
grain?: number;
|
|
28
|
+
/** Animation speed multiplier, 0 to 4. Default 1. */
|
|
29
|
+
speed?: number;
|
|
30
|
+
/** How much the effect warps, 0 to 1. Default 0.5. */
|
|
31
|
+
distortion?: number;
|
|
32
|
+
/** Pattern zoom, 0.25 to 4; above 1 makes features larger. Default 1. */
|
|
33
|
+
scale?: number;
|
|
34
|
+
/** Goo only: how far it reaches, 0 (a slight bulge) to 1 (a drip). Default 1. */
|
|
35
|
+
stretch?: number;
|
|
36
|
+
/** Goo only: how thick and slow it is, 0 to 1. Default 0.5. */
|
|
37
|
+
thickness?: number;
|
|
38
|
+
/** Default 'circle'. */
|
|
39
|
+
shape?: Shape;
|
|
40
|
+
/** Width of a ring that separates overlapping avatars, CSS px. Default 0. */
|
|
41
|
+
ring?: number;
|
|
42
|
+
/** Ring color, any CSS color. Default '#fff'. */
|
|
43
|
+
ringColor?: string;
|
|
44
|
+
/** Sets role="img" and aria-label. Without it the avatar is decorative. */
|
|
45
|
+
label?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AvatarInstance {
|
|
49
|
+
element: HTMLElement;
|
|
50
|
+
update(options: Partial<AvatarOptions>): void;
|
|
51
|
+
destroy(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Appends an avatar to `el`. */
|
|
55
|
+
export function avatar(el: Element, options: AvatarOptions): AvatarInstance;
|
|
56
|
+
|
|
57
|
+
export type CursorVariant = 'arrow' | 'avatar';
|
|
58
|
+
|
|
59
|
+
export interface CursorOptions extends Omit<AvatarOptions, 'size' | 'shape' | 'interaction' | 'label'> {
|
|
60
|
+
/** arrow: a two-color arrow with the avatar beside the name. avatar: the avatar is the pointer. Default arrow. */
|
|
61
|
+
variant?: CursorVariant;
|
|
62
|
+
/**
|
|
63
|
+
* Shown on the tag. Without one the tag shows the seed's anonymous name,
|
|
64
|
+
* like "Swift Otter" (see nameFor). false leaves the tag off.
|
|
65
|
+
*/
|
|
66
|
+
name?: string | false;
|
|
67
|
+
/** Draws the cursor larger, avatars included, for previews or big screens. Default 1. */
|
|
68
|
+
zoom?: number;
|
|
69
|
+
/** Where the tip goes, inside the positioned element the cursor was added to. */
|
|
70
|
+
x?: number;
|
|
71
|
+
y?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CursorInstance {
|
|
75
|
+
element: HTMLSpanElement;
|
|
76
|
+
/** Puts the tip at x, y. Cheap: call it on every pointer update. */
|
|
77
|
+
move(x: number, y: number): void;
|
|
78
|
+
update(options: Partial<CursorOptions>): void;
|
|
79
|
+
destroy(): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Appends a presence cursor to `el`, which should be positioned. */
|
|
83
|
+
export function cursor(el: Element, options: CursorOptions): CursorInstance;
|
|
84
|
+
|
|
85
|
+
/** The anonymous name a seed gets, like "Swift Otter": an adjective and an animal. The same seed always gets the same name. */
|
|
86
|
+
export function nameFor(seed: string): string;
|
|
87
|
+
|
|
88
|
+
/** Registers <crafted-avatar>. Attributes match the options, with ring-color for ringColor. */
|
|
89
|
+
export function defineAvatarElement(name?: string): void;
|
|
90
|
+
|
|
91
|
+
export interface StillOptions extends Omit<AvatarOptions, 'seed' | 'motion' | 'interaction' | 'stretch' | 'thickness' | 'label'> {
|
|
92
|
+
/** Pixels, up to 2048. Default 512. */
|
|
93
|
+
size?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One frame at the seed's phase, as a PNG data URL. */
|
|
97
|
+
export function toDataURL(seed: string, options?: StillOptions): Promise<string>;
|
|
98
|
+
/** One frame at the seed's phase, as a PNG blob. */
|
|
99
|
+
export function toBlob(seed: string, options?: StillOptions): Promise<Blob>;
|
|
100
|
+
|
|
101
|
+
/** What a seed maps to. `colors` are the colors it gives its effect, as hex, in the effect's roles. */
|
|
102
|
+
export function traits(seed: string, overrides?: { effect?: Effect | 'auto' }): {
|
|
103
|
+
effect: Effect;
|
|
104
|
+
/** The first color's hue, 0 to 359. */
|
|
105
|
+
hue: number;
|
|
106
|
+
/** 32 bits that pick the other colors. */
|
|
107
|
+
tone: number;
|
|
108
|
+
phase: number;
|
|
109
|
+
colors: string[];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Every effect. `auto` only picks from AUTO_EFFECTS. */
|
|
113
|
+
export const EFFECTS: readonly Effect[];
|
|
114
|
+
export const AUTO_EFFECTS: readonly Effect[];
|
package/types/react.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CSSProperties, HTMLAttributes, JSX } from 'react';
|
|
2
|
+
import type { AvatarOptions, CursorOptions } from './index.js';
|
|
3
|
+
|
|
4
|
+
export type AvatarProps = AvatarOptions & Omit<HTMLAttributes<HTMLSpanElement>, 'children'>;
|
|
5
|
+
|
|
6
|
+
export function Avatar(props: AvatarProps): JSX.Element;
|
|
7
|
+
|
|
8
|
+
export type StackSeed = string | (AvatarOptions & { style?: CSSProperties });
|
|
9
|
+
|
|
10
|
+
export interface AvatarStackProps extends Omit<AvatarOptions, 'seed' | 'label'> {
|
|
11
|
+
seeds: StackSeed[];
|
|
12
|
+
/** How many to show before +N. Default 4. */
|
|
13
|
+
max?: number;
|
|
14
|
+
/** How much each avatar tucks under the one before, as a fraction of size. Default 0.25. */
|
|
15
|
+
overlap?: number;
|
|
16
|
+
/** The group's accessible name. Default "N people". */
|
|
17
|
+
label?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AvatarStack(props: AvatarStackProps): JSX.Element;
|
|
23
|
+
|
|
24
|
+
export type CursorProps = CursorOptions & { className?: string; style?: CSSProperties };
|
|
25
|
+
|
|
26
|
+
/** A presence cursor with the avatar built in; the tip lands on x, y. */
|
|
27
|
+
export function Cursor(props: CursorProps): JSX.Element;
|