@cruxgarden/plasma-ui 0.2.4 → 0.5.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/PlasmaProvider.tsx
2
- import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
 
4
4
  // src/shaders.ts
5
5
  var DEFAULT_MAX_SHAPES = 16;
@@ -9,7 +9,7 @@ in vec2 p; void main(){ gl_Position = vec4(p, 0., 1.); }`;
9
9
  function makeShaders(MAX_SHAPES) {
10
10
  const common = `
11
11
  precision highp float;
12
- uniform vec2 uRes; uniform vec4 uView; uniform float uScale, uTime, uGoo, uEnergy, uLight, uMouseAmt, uDropR, uAmbient, uScroll, uVisc, uFlow;
12
+ uniform vec2 uRes; uniform vec4 uView; uniform float uScale, uTime, uGoo, uEnergy, uLight, uMouseAmt, uDropR, uAmbient, uScroll, uVisc, uFlow, uTension, uThick;
13
13
  uniform vec2 uMouse;
14
14
  uniform vec4 uP[${MAX_SHAPES}]; uniform vec4 uR[${MAX_SHAPES}]; uniform float uF[${MAX_SHAPES}]; uniform vec4 uT[${MAX_SHAPES}]; uniform float uFr[${MAX_SHAPES}]; uniform float uEl[${MAX_SHAPES}]; uniform float uSolo[${MAX_SHAPES}];
15
15
  uniform int uCount;
@@ -128,6 +128,7 @@ uniform vec2 uImgRes;
128
128
  uniform float uHasImg;
129
129
  uniform vec3 uBgColor;
130
130
  uniform float uBgSolid;
131
+ uniform float uClear;
131
132
  out vec4 o;
132
133
  vec3 bg(vec2 p){
133
134
  p.y += uScroll;
@@ -161,6 +162,15 @@ void main(){
161
162
  o = vec4(uBgColor * (1. + shade), 1.);
162
163
  return;
163
164
  }
165
+ if (uHasImg > .5 && uClear > .5) {
166
+ // a clear ground samples what is beneath it exactly - no swirl, no warp -
167
+ // so a refraction of it lines up with the page it sits on
168
+ float sc = max(uView.z / uImgRes.x, uView.w / uImgRes.y);
169
+ vec2 uv = (p - uView.xy - .5*uView.zw) / (uImgRes * sc) + .5;
170
+ o = vec4(texture(uImg, clamp(uv, 0., 1.)).rgb, 1.);
171
+ return;
172
+ }
173
+ if (uClear > .5) { o = vec4(0., 0., 0., 1.); return; }
164
174
  if (uHasImg > .5) {
165
175
  // image background: slow swirl plus the pulse warp above
166
176
  vec2 q = bp;
@@ -178,13 +188,39 @@ void main(){
178
188
  const compFrag = `#version 300 es
179
189
  ${common}
180
190
  uniform sampler2D uH, uS, uTint, uBg, uBgM, uBgH, uFrost;
181
- uniform float uRefract, uDisp, uRim, uRimMode, uRimWidth, uSpec, uHair;
191
+ uniform float uRefract, uDisp, uRim, uRimMode, uRimWidth, uSpec, uHair, uShim, uGlow, uWash, uGrain;
192
+ // Which material the surfaces are made of, and the one light every opaque one
193
+ // reads. Glass fakes its lighting off the pointer because you see through it;
194
+ // the moment anything is opaque, two materials disagreeing about where the sun
195
+ // is looks broken in a way no single shader can fix. So there is one light.
196
+ uniform float uMat;
197
+ // 1 when the ground is clear: alpha is the surface's coverage (and its
198
+ // shadow's darkness), so the canvas can sit above other content.
199
+ uniform float uClear;
200
+ uniform vec3 uLightDir;
201
+ // Surface finish, shared by every material that has one: 0 is a mirror, 1 is
202
+ // chalk. Anisotropy stretches the highlight along the grain \u2014 brushed metal
203
+ // and varnished wood both need it, and neither reads right without it.
204
+ uniform float uRough, uAniso;
205
+ // How thick a panel is, in CSS px, and how much the material rounds itself
206
+ // against that. Mercury has enormous surface tension, so a bead pulls toward
207
+ // a sphere; tension drives both the corner radius and how eagerly two beads
208
+ // merge into one body.
209
+ // The edge treatment. A rounded rectangle is right for a liquid, and wrong for
210
+ // almost everything else: stone chips, cloud billows, metal is cut. These
211
+ // displace the silhouette itself \u2014 amplitude in CSS px, scale in cycles \u2014
212
+ // before anything decides what is inside, so the outline, the rim, the shadow
213
+ // and the material all agree about where the panel ends.
214
+ uniform float uEdge, uEdgeScale, uEdgeSharp;
182
215
  uniform vec3 uRimColor;
183
216
  out vec4 o;
184
217
 
185
218
  vec3 pal(float t){ return .5 + .5*cos(6.2831*(t + vec3(0., .33, .67))); }
186
219
  float H(vec2 uv){ return texture(uH, uv).r; }
187
- vec2 uvAt(vec2 q){ return vec2(q.x, uRes.y - q.y) / uRes; }
220
+ // Clamped, because a reflection offset is far larger than a refraction one:
221
+ // metal sweeps up to 150px and walks straight off the texture, where
222
+ // CLAMP_TO_EDGE smears the last row of pixels into arcs across the panel.
223
+ vec2 uvAt(vec2 q){ return clamp(vec2(q.x, uRes.y - q.y) / uRes, vec2(.001), vec2(.999)); }
188
224
  // background seen through plasma with frost f: sharp, then medium, then heavy blur
189
225
  vec3 seen(vec2 q, float f){
190
226
  vec2 u = uvAt(q);
@@ -193,6 +229,248 @@ vec3 seen(vec2 q, float f){
193
229
  vec3 soft = mix(texture(uBgM, u).rgb, texture(uBgH, u).rgb, smoothstep(.45, 1., f));
194
230
  return mix(sharp, soft, smoothstep(0., .45, f));
195
231
  }
232
+ // \u2500\u2500 Shading kit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
233
+ // A real microfacet BRDF, not pow(ndl, n). GGX + height-correlated Smith +
234
+ // Schlick is what every modern renderer uses, and it is the single biggest
235
+ // difference between a material that reads as itself and one that reads as
236
+ // coloured plastic.
237
+ float D_GGX(float ndh, float a){ float a2 = a*a; float d = ndh*ndh*(a2-1.)+1.; return a2/(3.14159265*d*d + 1e-7); }
238
+ // Anisotropic GGX: the highlight stretches along a direction. This is the tell
239
+ // for wood and for brushed metal \u2014 an isotropic highlight on wood looks wrong
240
+ // even when nothing else does.
241
+ float D_GGXaniso(float ndh, float tdh, float bdh, float ax, float ay){
242
+ float d = tdh*tdh/(ax*ax) + bdh*bdh/(ay*ay) + ndh*ndh;
243
+ return 1./(3.14159265*ax*ay*d*d + 1e-7);
244
+ }
245
+ float V_SmithGGX(float ndv, float ndl, float a){
246
+ float a2 = a*a;
247
+ float gv = ndl*sqrt(ndv*ndv*(1.-a2)+a2);
248
+ float gl = ndv*sqrt(ndl*ndl*(1.-a2)+a2);
249
+ return .5/max(gv+gl, 1e-5);
250
+ }
251
+ vec3 F_Schlick(vec3 f0, float u){ return f0 + (1.-f0)*pow(clamp(1.-u,0.,1.), 5.); }
252
+
253
+ // Lighting has to happen in linear light. Doing it in gamma space is the
254
+ // oldest way to make a render look cheap: highlights clip early and midtones
255
+ // go chalky. sqrt/square is the standard cheap sRGB pair.
256
+ vec3 toLinear(vec3 c){ return c*c; }
257
+ vec3 toSrgb(vec3 c){ return sqrt(max(c, 0.)); }
258
+ // ACES filmic, fitted. Keeps a bright specular from flattening into white.
259
+ vec3 tonemap(vec3 x){ return clamp((x*(2.51*x+.03))/(x*(2.43*x+.59)+.14), 0., 1.); }
260
+
261
+ // Plain fbm reads as fractal mush. Warping the domain by another fbm is what
262
+ // makes grain and rock look grown rather than generated \u2014 the background
263
+ // shader already leans on this.
264
+ vec2 warp(vec2 q, float k){
265
+ return q + k*vec2(fbm(q + vec2(1.7, 9.2)), fbm(q + vec2(5.3, 2.8)));
266
+ }
267
+
268
+ // Metal only reads as metal when it reflects something with structure, and a
269
+ // page background is nearly uniform \u2014 reflect it alone and you get grey paint
270
+ // with a shiny rim. So reflective materials also see a small studio: a sky
271
+ // gradient, a horizon, and a sun in the same direction as the light. Roughness
272
+ // widens the sun and flattens the band, which is what a prefiltered
273
+ // environment map does for real.
274
+ vec3 envmap(vec3 R, float rough){
275
+ float h = clamp(R.y * .5 + .5, 0., 1.);
276
+ vec3 sky = mix(vec3(.62, .70, .84), vec3(.10, .13, .19), pow(1. - h, 1.5));
277
+ vec3 ground = vec3(.055, .05, .048);
278
+ vec3 c = mix(ground, sky, smoothstep(.40, .60, h));
279
+ vec3 L = normalize(uLightDir);
280
+ float sun = pow(max(dot(R, L), 0.), mix(260., 5., rough));
281
+ c += vec3(1., .96, .88) * sun * (1. - rough * .7) * 2.2;
282
+ // the horizon band is what gives a curved metal edge its sweep
283
+ c += vec3(.75, .82, .95) * (1. - smoothstep(.0, .22, abs(h - .54))) * .45 * (1. - rough * .55);
284
+ return c;
285
+ }
286
+
287
+ // \u2500\u2500 The scene as a solid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
288
+ // Everything above shades a flat card: a 2D silhouette, a perturbed normal,
289
+ // a texture lookup. That is skinning, and it is why wood came out looking
290
+ // like a wood-effect laminate. A material reads as a material when there is a
291
+ // body to light \u2014 so the same shapes are extruded into a slab and the view ray
292
+ // is marched through it, the way a game would.
293
+ //
294
+ // The 2D mask is a free conservative bound: outside it there is nothing to
295
+ // hit, so the march never starts.
296
+ float map3(vec3 q, float tension, float thick){
297
+ float d = 1e5;
298
+ for (int i = 0; i < uCount; i++) {
299
+ float f = uF[i];
300
+ if (f < .005) continue;
301
+ vec2 c = uP[i].xy;
302
+ vec2 h = uP[i].zw * f;
303
+ float rmax = max(max(uR[i].x, uR[i].y), max(uR[i].z, uR[i].w));
304
+ // The footprint is the element's, exactly as plasma's is. Mercury is not
305
+ // a blob that swallows the layout \u2014 it is a panel, poured. Tension belongs
306
+ // to the edge and to the merge, not to the shape: pulling the whole
307
+ // footprint toward a disc threw the panel away and left content floating
308
+ // over nothing.
309
+ float d2 = sdBoxG(q.xy - c, h, min(uR[i], vec4(min(h.x, h.y))), rmax).x;
310
+ // Extrude, with the rim rolled over so the slab has a shoulder rather than
311
+ // a cut edge. Tension fattens that roll \u2014 where a bead of mercury reads as
312
+ // poured is its edge, not its outline.
313
+ float roll = thick * (.22 + .50 * tension);
314
+ vec2 w = vec2(d2 + roll, abs(q.z) - (thick - roll));
315
+ float de = min(max(w.x, w.y), 0.) + length(max(w, 0.)) - roll;
316
+ d = (i == 0) ? de : smin(d, de, uGoo * (.6 + 1.8 * tension));
317
+ }
318
+ // Two slow swells across the whole body, displacing the surface itself
319
+ // rather than tilting a normal \u2014 the march finds a moving surface, so the
320
+ // reflection travels over real waves. This is what separates water from a
321
+ // picture of water.
322
+ // Displacing a distance field costs it the property the march depends on:
323
+ // d is no longer a safe distance, so a full step overshoots the surface and
324
+ // the ray lands inside, which is the mottled crust this produced at first.
325
+ // The amplitude is kept small and the march below steps at a fraction, which
326
+ // is the standard price of displacement.
327
+ if (tension > .01) {
328
+ float wv = (fbm(q.xy * .0075 + vec2(uTime * .11, uTime * .06)) - .5) * 1.6
329
+ + (fbm(q.xy * .019 - vec2(uTime * .08, uTime * .13)) - .5) * .6;
330
+ d -= wv * thick * .07 * tension;
331
+ }
332
+ return d;
333
+ }
334
+
335
+ /**
336
+ * A crystalline crown: the panel's face is a regular triangular lattice, and
337
+ * every triangle is a flat facet with its own plane.
338
+ *
339
+ * Regular, not random \u2014 a Delaunay scatter reads as shattered safety glass,
340
+ * and a lattice reads as something cut. The heights at the lattice corners are
341
+ * hashed, so the facets tilt differently while the pattern stays symmetric.
342
+ *
343
+ * The normal comes out exact rather than differenced, which is what makes the
344
+ * faces crisp: a facet is a plane, so its normal is constant across it, and
345
+ * there is nothing for a march to stipple.
346
+ */
347
+ vec3 facetPlane(vec2 uv, float amp, out float height){
348
+ vec2 c = floor(uv), f = fract(uv);
349
+ float h00 = hash(c) * amp;
350
+ float h10 = hash(c + vec2(1., 0.)) * amp;
351
+ float h01 = hash(c + vec2(0., 1.)) * amp;
352
+ float h11 = hash(c + vec2(1., 1.)) * amp;
353
+ float dx, dy;
354
+ // Which way the cell's diagonal runs is decided per cell. One diagonal
355
+ // everywhere reads as corduroy; alternating reads as cut.
356
+ bool flip = hash(c + 91.7) > .5;
357
+ bool lower = flip ? (f.x + f.y < 1.) : (f.x > f.y);
358
+ if (lower) {
359
+ height = h00 + (h10 - h00) * f.x + (h01 - h00) * f.y;
360
+ dx = h10 - h00; dy = h01 - h00;
361
+ } else {
362
+ height = h11 + (h01 - h11) * (1. - f.x) + (h10 - h11) * (1. - f.y);
363
+ dx = h11 - h01; dy = h11 - h10;
364
+ }
365
+ return normalize(vec3(-dx, -dy, 1.));
366
+ }
367
+
368
+ /**
369
+ * The crystal's top surface, as a height in page px, plus the exact plane
370
+ * normal of whichever facet the point lies on. This is a real surface: the
371
+ * march below has to find where the ray crosses it, so the facets occlude one
372
+ * another, break the silhouette, and shadow each other. A normal returned
373
+ * without an intersection would be a normal map, which is what the version
374
+ * before this was.
375
+ */
376
+ float crownZ(vec2 xy, float gcut, float amp, float thick, out vec3 nrm){
377
+ float h;
378
+ nrm = facetPlane(xy / gcut, amp / gcut, h);
379
+ return thick + h * gcut;
380
+ }
381
+
382
+ // \u2500\u2500 The gem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
383
+ // Facet directions, spread over a hemisphere and mirrored, standing in for a
384
+ // cut stone's crown and pavilion. Generated rather than tabulated so the count
385
+ // is a constant to change, not a table to rewrite.
386
+ vec3 facetN(int i){
387
+ float fi = float(i);
388
+ float a = fi * 2.3999632; // golden angle, so they do not band
389
+ float z = mix(.22, .92, fract(fi * .6180339));
390
+ float r = sqrt(max(0., 1. - z * z));
391
+ return normalize(vec3(cos(a) * r, sin(a) * r, z));
392
+ }
393
+
394
+ /**
395
+ * The panel as a piece of gem. The slab is the element's own footprint,
396
+ * extruded; the facets are half-spaces cut through it.
397
+ *
398
+ * The cuts are anchored in PAGE space, not the panel's, which is the whole
399
+ * trick: a facet plane runs on across the gap and slices the next panel too,
400
+ * so the panels read as pieces carved out of one stone rather than as
401
+ * separate gems that happen to match.
402
+ */
403
+ float mapGem(vec3 q, float thick, float cut){
404
+ float d = 1e5;
405
+ for (int i = 0; i < uCount; i++) {
406
+ float f = uF[i];
407
+ if (f < .005) continue;
408
+ vec2 c = uP[i].xy;
409
+ vec2 h = uP[i].zw * f;
410
+ float rmax = max(max(uR[i].x, uR[i].y), max(uR[i].z, uR[i].w));
411
+ float d2 = sdBoxG(q.xy - c, h, min(uR[i], vec4(min(h.x, h.y))), rmax).x;
412
+ vec2 w = vec2(d2, abs(q.z) - thick);
413
+ float de = min(max(w.x, w.y), 0.) + length(max(w, 0.)) - 1.5;
414
+ d = (i == 0) ? de : min(d, de);
415
+ }
416
+ // Cut the stone. Each plane repeats on a lattice so the whole page is one
417
+ // crystal the panels are taken out of. Six directions, and the lattice is
418
+ // wide \u2014 a few facets across a panel, not hundreds. Cut it finely and a gem
419
+ // stops being a gem and becomes gravel, which is exactly what nine planes at
420
+ // 34px produced on the first try.
421
+ for (int i = 0; i < 6; i++) {
422
+ vec3 nn = facetN(i);
423
+ if (fract(float(i) * .5) > .25) nn.z = -nn.z; // mirror half of them below
424
+ // The plane sits near the far end of its lattice cell, so a cut trims a
425
+ // corner instead of slicing the body in half. Six planes at 70% of the
426
+ // cell removed most of the volume and left the panels as floating shards.
427
+ // A gem is faceted, not shattered.
428
+ float hgt = dot(q, nn);
429
+ float plane = floor(hgt / cut) * cut + cut * .965;
430
+ d = max(d, hgt - plane);
431
+ }
432
+ return d;
433
+ }
434
+
435
+ vec3 gemNormal(vec3 q, float thick, float cut){
436
+ vec2 e = vec2(.4, 0.);
437
+ return normalize(vec3(
438
+ mapGem(q + e.xyy, thick, cut) - mapGem(q - e.xyy, thick, cut),
439
+ mapGem(q + e.yxy, thick, cut) - mapGem(q - e.yxy, thick, cut),
440
+ mapGem(q + e.yyx, thick, cut) - mapGem(q - e.yyx, thick, cut)
441
+ ));
442
+ }
443
+
444
+ vec3 normal3(vec3 q, float tension, float thick){
445
+ vec2 e = vec2(.75, 0.);
446
+ return normalize(vec3(
447
+ map3(q + e.xyy, tension, thick) - map3(q - e.xyy, tension, thick),
448
+ map3(q + e.yxy, tension, thick) - map3(q - e.yxy, tension, thick),
449
+ map3(q + e.yyx, tension, thick) - map3(q - e.yyx, tension, thick)
450
+ ));
451
+ }
452
+
453
+ // Worley/Voronoi: nearest cell, second nearest, and the cell's id. The gap
454
+ // between the two is the distance to a cell wall, which is what draws the
455
+ // seams of a crystal without any geometry.
456
+ vec4 voronoi(vec2 q){
457
+ vec2 ip = floor(q), fp = fract(q);
458
+ float d1 = 8., d2 = 8.;
459
+ vec2 id = vec2(0.);
460
+ for (int j = -1; j <= 1; j++) {
461
+ for (int i = -1; i <= 1; i++) {
462
+ vec2 g = vec2(float(i), float(j));
463
+ vec2 cell = ip + g;
464
+ vec2 o = vec2(hash(cell), hash(cell + 31.7));
465
+ vec2 r = g + o - fp;
466
+ float d = dot(r, r);
467
+ if (d < d1) { d2 = d1; d1 = d; id = cell; }
468
+ else if (d < d2) { d2 = d; }
469
+ }
470
+ }
471
+ return vec4(sqrt(d1), sqrt(d2), id);
472
+ }
473
+
196
474
  // B-spline bicubic from four bilinear taps keeps curved outlines round.
197
475
  float S(vec2 uv){
198
476
  vec2 ts = vec2(textureSize(uS, 0));
@@ -219,9 +497,23 @@ void main(){
219
497
  float s = S(uv);
220
498
  vec2 gs = vec2(S(uv+vec2(tx.x,0.)) - S(uv-vec2(tx.x,0.)), S(uv+vec2(0.,tx.y)) - S(uv-vec2(0.,tx.y))) / (2.*cssPerTexel);
221
499
  float sd = clamp((s - .5) / max(length(gs), 1e-3), -60., 60.);
500
+ if (uEdge > .01) {
501
+ // Two octaves: the first gives the large shape (billows, chips), the
502
+ // second the fine break-up. Warping keeps it from reading as a sine wave
503
+ // stamped around the outline.
504
+ vec2 ep = warp(p * uEdgeScale, .7);
505
+ float e1 = fbm(ep) - .5;
506
+ float e2 = fbm(ep * 3.3 + 17.) - .5;
507
+ float disp = e1 * 1.5 + e2 * .55;
508
+ // uEdgeSharp 0 leaves it rolling; 1 pushes it toward flats and points,
509
+ // which is what reads as chipped rather than melted.
510
+ disp = mix(disp, sign(disp) * pow(abs(disp) * 2., .55) * .5, clamp(uEdgeSharp, 0., 1.));
511
+ sd += disp * uEdge;
512
+ }
222
513
 
223
514
  vec3 back = texture(uBg, uv).rgb;
224
515
  float grain = 1.;
516
+ float cover = 0.;
225
517
 
226
518
  // shadow: offset and strength follow the surface's elevation
227
519
  float msk = max(texture(uS, uv).r, 1e-3);
@@ -235,7 +527,7 @@ void main(){
235
527
  // Palette phases run on viewport position, like the grain: a frame drawn for
236
528
  // a taller region must colour its viewport band exactly as the live frame does.
237
529
  vec2 vp = p - uView.xy;
238
- col += pal(uTime*.03 + vp.x/1400.) * .06 * smoothstep(.0, .45, hHere) * (1.+uEnergy);
530
+ col += pal(uTime*.03 + vp.x/1400.) * .06 * smoothstep(.0, .45, hHere) * (1.+uEnergy) * uGlow;
239
531
 
240
532
  if(sd > -2.){
241
533
  vec2 th = 2. / vec2(textureSize(uH, 0));
@@ -255,7 +547,12 @@ void main(){
255
547
  seen(p + off, fr).g,
256
548
  seen(p + off*(1.-disp), fr).b
257
549
  );
550
+ // uWash 0 hands the background through untouched - glass with no cast of
551
+ // its own. 1 is the original desaturate-and-lift that gives the material
552
+ // its body.
553
+ vec3 clearRefr = refr;
258
554
  refr = mix(refr, vec3(dot(refr, vec3(.333))), .18) * mix(1.08, .9, uLight) + .03*(1.-uLight);
555
+ refr = mix(clearRefr, refr, uWash);
259
556
  // frosted: milkier and a little brighter
260
557
  refr = mix(refr, mix(refr, vec3(dot(refr, vec3(.333))), .25) * mix(1.12, .97, uLight) + mix(.05, .03, uLight), fr);
261
558
  float hl = 1. - .55*uLight;
@@ -276,21 +573,321 @@ void main(){
276
573
  if (uRimMode > .5 && uRimMode < 1.5) rimCol = uRimColor * facing * 1.4;
277
574
  else if (uRimMode > 1.5) rimCol = tcol * facing * 1.4;
278
575
 
576
+ // A 3D normal from the 2D height gradient: flat in the middle, turning
577
+ // outward and down across the bevel. Every material shades from this, and
578
+ // the opaque ones perturb it further with their own detail.
579
+ vec3 Nb = normalize(vec3(n * (.30 + .70 * bevel), max(.20, 1. - bevel)));
580
+ vec3 Ldir = normalize(uLightDir);
581
+ vec3 V = vec3(0., 0., 1.); // orthographic view, straight on
279
582
  vec3 plasma = refr;
280
- plasma += rimCol * fres * .45 * hl * uRim;
281
- plasma += vec3(1.) * spec * .75 * hl * uSpec;
282
- // faint shimmer across the body; fades out as the tint becomes opaque
283
- plasma += pal(uTime*.04 + vp.y/900. + uEnergy*.3) * .05 * lift * (1.+uEnergy*2.) * hl * (1. - talpha);
284
- vec3 hairCol = uRimMode > .5 ? mix(vec3(1.), rimCol / 1.4, .6) : vec3(.9,.95,1.);
285
- plasma += hairCol * (1.-smoothstep(0., 1.6, abs(sd - .7))) * .4 * hl * uHair;
286
-
287
583
  float a = smoothstep(-.8, .8, sd);
584
+
585
+ if (uMat < .5) {
586
+ // \u2500\u2500 plasma \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
587
+ plasma += rimCol * fres * .45 * hl * uRim;
588
+ plasma += vec3(1.) * spec * .75 * hl * uSpec;
589
+ // faint shimmer across the body; fades out as the tint becomes opaque
590
+ plasma += pal(uTime*.04 + vp.y/900. + uEnergy*.3) * .05 * lift * (1.+uEnergy*2.) * hl * (1. - talpha) * uShim;
591
+ vec3 hairCol = uRimMode > .5 ? mix(vec3(1.), rimCol / 1.4, .6) : vec3(.9,.95,1.);
592
+ plasma += hairCol * (1.-smoothstep(0., 1.6, abs(sd - .7))) * .4 * hl * uHair;
593
+ } else if (uMat < 1.5) {
594
+ // \u2500\u2500 crystal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
595
+ // Actual 3D. The panel is a solid whose top is a cut crown \u2014 a lattice
596
+ // of flat facets, anchored in PAGE space so a facet runs on across the
597
+ // gap into the next panel. The view ray is marched until it crosses that
598
+ // surface, so the facets genuinely occlude each other, break the
599
+ // silhouette where they rise and fall, and shadow one another.
600
+ //
601
+ // A height field is marched by stepping and testing which side of the
602
+ // surface you are on, then bisecting \u2014 not by sphere tracing, which a
603
+ // piecewise-planar field is hostile to.
604
+ float gcut = max(uThick * 2.4, 52.);
605
+ float amp = gcut * .45;
606
+ float gthick = max(uThick, 10.);
607
+
608
+ vec3 gro = vec3(p, gthick + amp * 1.6 + 4.);
609
+ vec3 grd = normalize(vec3((p - uMouse) * .00055, -1.));
610
+
611
+ float tPrev = 0., tHit = -1.;
612
+ vec3 nHit = vec3(0., 0., 1.);
613
+ float stepLen = max(amp * .16, 1.2);
614
+ for (int i = 0; i < 64; i++) {
615
+ float t = float(i) * stepLen;
616
+ vec3 q = gro + grd * t;
617
+ // Outside the panel's footprint there is no stone to hit.
618
+ if (scene(q.xy) > 0.) { tPrev = t; continue; }
619
+ vec3 nn;
620
+ float sz = crownZ(q.xy, gcut, amp, gthick, nn);
621
+ if (q.z <= sz) {
622
+ // Crossed it. Bisect for the exact facet plane.
623
+ float lo = tPrev, hi = t;
624
+ for (int k = 0; k < 6; k++) {
625
+ float mid = (lo + hi) * .5;
626
+ vec3 qm = gro + grd * mid;
627
+ vec3 nm;
628
+ float szm = crownZ(qm.xy, gcut, amp, gthick, nm);
629
+ if (qm.z <= szm) { hi = mid; nHit = nm; } else { lo = mid; }
630
+ }
631
+ tHit = hi;
632
+ break;
633
+ }
634
+ tPrev = t;
635
+ }
636
+
637
+ if (tHit < 0.) {
638
+ a = 0.; // the ray passed over the stone
639
+ } else {
640
+ vec3 q0 = gro + grd * tHit;
641
+ vec3 Nf = nHit;
642
+ float ndv = max(dot(Nf, -grd), 1e-3);
643
+ float ndl = max(dot(Nf, Ldir), 0.);
644
+ vec3 H = normalize(Ldir - grd);
645
+
646
+ // Shadow: march back toward the light across the crown. This is the
647
+ // thing a normal map can never do \u2014 one facet standing in another's
648
+ // light.
649
+ float shade = 1.;
650
+ for (int i = 1; i <= 10; i++) {
651
+ vec3 qs = q0 + Ldir * (float(i) * amp * .30);
652
+ vec3 nn;
653
+ float sz = crownZ(qs.xy, gcut, amp, gthick, nn);
654
+ if (qs.z < sz - .5 && scene(qs.xy) < 0.) { shade = .25; break; }
655
+ }
656
+
657
+ // Through the stone: refract in, cross the body, refract out at the
658
+ // underside.
659
+ float ior = 1.85;
660
+ vec3 rin = refract(grd, Nf, 1. / ior);
661
+ vec3 nn2;
662
+ float h2;
663
+ vec3 Nb2 = facetPlane((q0.xy + rin.xy * gthick * 2.) / gcut + 23.7, amp / gcut, h2);
664
+ vec3 rout = refract(rin, -Nb2, ior);
665
+ if (dot(rout, rout) < 1e-6) rout = reflect(rin, -Nb2);
666
+
667
+ float disp = .06 * max(uDisp, .001) * 6.;
668
+ vec2 off2 = rout.xy * gthick * 4.;
669
+ vec3 thru = toLinear(vec3(
670
+ seen(p + off2 * (1. + disp), fr).r,
671
+ seen(p + off2, fr).g,
672
+ seen(p + off2 * (1. - disp), fr).b
673
+ ));
674
+
675
+ vec3 body = toLinear(mix(vec3(.62, .48, .92), tcol, talpha));
676
+ vec3 lit = body * (.10 + 1.15 * ndl * shade);
677
+ lit += thru * body * 1.2 * (.25 + .75 * ndv);
678
+
679
+ vec3 F0 = vec3(.13);
680
+ float fres = pow(1. - ndv, 4.);
681
+ lit = mix(lit, envmap(reflect(grd, Nf), .02), F0.x + (1. - F0.x) * fres * .9);
682
+
683
+ float aa = .018 * .018;
684
+ lit += F_Schlick(F0, max(dot(H, -grd), 0.))
685
+ * D_GGX(max(dot(Nf, H), 0.), aa) * V_SmithGGX(ndv, ndl, aa) * ndl * 26. * uSpec * shade;
686
+ plasma = toSrgb(tonemap(lit));
687
+ a = 1.;
688
+ }
689
+ } else if (uMat < 3.5) {
690
+ bool merc = uMat > 2.5;
691
+ if (merc) {
692
+ // \u2500\u2500 mercury \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
693
+ // Marched, not painted. The ray enters the slab, and every normal
694
+ // below comes from the solid's own gradient \u2014 which is what makes a
695
+ // bead read as a bead instead of a rectangle with a chrome gradient
696
+ // on it. Surface tension does the rest: the footprint rounds toward a
697
+ // disc, and two panels within reach pull into one body with a neck.
698
+ float thick = max(uThick, 8.);
699
+ float tension = clamp(uTension, 0., 1.);
700
+ vec3 ro = vec3(p, thick * 3.5);
701
+ vec3 rd = normalize(vec3((p - uMouse) * .00035, -1.)); // a hair of perspective
702
+ float t = 0., hit = -1.;
703
+ for (int i = 0; i < 96; i++) {
704
+ vec3 q = ro + rd * t;
705
+ float dd = map3(q, tension, thick);
706
+ if (dd < .35) { hit = t; break; }
707
+ t += max(dd * .30, .25);
708
+ if (t > thick * 8.) break;
709
+ }
710
+ if (hit < 0.) {
711
+ a = 0.; // the march missed: leave the ground
712
+ } else {
713
+ vec3 q = ro + rd * hit;
714
+ vec3 Nm = normal3(q, tension, thick);
715
+ float ndv = max(dot(Nm, -rd), 1e-3);
716
+ float ndl = max(dot(Nm, Ldir), 0.);
717
+ vec3 H = normalize(Ldir - rd);
718
+ vec3 R = reflect(rd, Nm);
719
+
720
+ float rough = .04;
721
+ // A real reflection off a real normal, of the studio and of what is
722
+ // actually behind the bead.
723
+ vec2 roff = R.xy * 210.;
724
+ vec3 env = mix(envmap(R, rough), toLinear(seen(p - roff, rough)), .30);
725
+ vec3 F0 = mix(vec3(.86, .87, .90), toLinear(tcol), talpha);
726
+ vec3 Fr = F_Schlick(F0, ndv);
727
+ float aa = max(rough * rough, 1e-4);
728
+ vec3 sun = F_Schlick(F0, max(dot(H, -rd), 0.))
729
+ * D_GGX(max(dot(Nm, H), 0.), aa)
730
+ * V_SmithGGX(ndv, ndl, aa) * ndl;
731
+ vec3 me = env * Fr + sun * 16. * uSpec;
732
+ me += F0 * .03;
733
+ // Shimmer comes from the modelled swell moving under a sharp
734
+ // highlight, not from a noise field tinting the surface. The only
735
+ // colour added is thin-film interference, which is a function of the
736
+ // angle the geometry presents \u2014 no texture in it.
737
+ me += toLinear(pal(pow(1. - ndv, 1.6) * 1.4)) * .12 * uShim * pow(1. - ndv, .9);
738
+ plasma = toSrgb(tonemap(me));
739
+ // The silhouette is the march's, so a bead that has pulled away from
740
+ // the rectangle actually looks pulled away.
741
+ a = 1.;
742
+ }
743
+ } else {
744
+ // \u2500\u2500 metal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
745
+ // Milled, not poured: flat stock with a cut edge. Still the screen
746
+ // space path, which is honest for a flat sheet.
747
+ float rough = clamp(uRough, .04, .95);
748
+ vec3 Nm = Nb;
749
+ float aniso = uAniso;
750
+ if (aniso > .01) {
751
+ float brush = fbm(vec2(p.x * .9, p.y * 22.)) - .5;
752
+ Nm = normalize(Nm + vec3(0., brush * aniso * .55, 0.));
753
+ }
754
+ float mr = fbm(p * .035) - .5;
755
+ Nm = normalize(Nm + vec3(mr * .16, (fbm(p * .035 + 11.) - .5) * .16, 0.));
756
+ float ndv = max(dot(Nm, V), 1e-3);
757
+ float ndl = max(dot(Nm, Ldir), 0.);
758
+ vec3 H = normalize(Ldir + V);
759
+ vec3 R = reflect(-V, Nm);
760
+ vec2 roff = R.xy * pow(bevel, .9) * 150. * (1. - rough * .5);
761
+ vec3 env = mix(envmap(R, rough), toLinear(seen(p - roff, rough)), .22);
762
+ vec3 F0 = mix(vec3(.95, .93, .88), toLinear(tcol), talpha);
763
+ vec3 Fr = F_Schlick(F0, ndv);
764
+ float aa = max(rough * rough, 1e-3);
765
+ float D = aniso > .01
766
+ ? D_GGXaniso(max(dot(Nm, H), 0.), dot(vec3(1.,0.,0.), H), dot(vec3(0.,1.,0.), H), aa * (1. + aniso * 3.), aa)
767
+ : D_GGX(max(dot(Nm, H), 0.), aa);
768
+ vec3 sun = F_Schlick(F0, max(dot(H, V), 0.)) * D * V_SmithGGX(ndv, ndl, aa) * ndl;
769
+ vec3 me = env * Fr + sun * 14. * uSpec;
770
+ me += F0 * .035;
771
+ plasma = toSrgb(tonemap(me));
772
+ }
773
+ } else if (uMat < 4.5) {
774
+ // \u2500\u2500 wood \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
775
+ // Grain lives in page coordinates, so a panel that resizes keeps its
776
+ // grain instead of stretching it \u2014 the first thing an opaque natural
777
+ // material asks for that glass never did.
778
+ vec2 wp = p * .0021;
779
+ // Rings are warped, then squeezed across one axis: straight fbm reads as
780
+ // camouflage, warped fbm reads as a board cut from a trunk.
781
+ vec2 wq = warp(vec2(wp.x * .40, wp.y * 4.2), .55);
782
+ float ring = fract(fbm(wq) * 2.05);
783
+ float band = abs(ring - .5) * 2.;
784
+ float pore = fbm(vec2(p.x * .18, p.y * .022));
785
+ float h0 = band * .7 + pore * .3;
786
+ // Detail normal by finite difference of that height, so the grain
787
+ // actually catches the light instead of being painted on.
788
+ vec2 e = vec2(1.4, 0.);
789
+ float hx = fract(fbm(warp(vec2((wp.x + e.x*.0021) * .40, wp.y * 4.2), .55)) * 2.05);
790
+ float hy = fract(fbm(warp(vec2(wp.x * .40, (wp.y + e.x*.0021) * 4.2), .55)) * 2.05);
791
+ hx = abs(hx - .5) * 2.; hy = abs(hy - .5) * 2.;
792
+ vec3 Nw = normalize(Nb + vec3((hx - band) * .85, (hy - band) * .85, 0.));
793
+
794
+ // Real boards are lower contrast than the first instinct: a wide dark-to
795
+ // -light sweep reads as charring, not as timber.
796
+ vec3 lightWood = toLinear(vec3(.62, .45, .28));
797
+ vec3 darkWood = toLinear(vec3(.40, .26, .145));
798
+ // A board is mostly one tone with the grain reading as a whisper over it.
799
+ vec3 alb = mix(darkWood, lightWood, smoothstep(.30, .70, band) * .55 + .22);
800
+ alb *= .95 + .09 * pore;
801
+ alb = mix(alb, toLinear(tcol), talpha * .6);
802
+
803
+ float ndv = max(dot(Nw, V), 1e-3);
804
+ float ndl = max(dot(Nw, Ldir), 0.);
805
+ vec3 H = normalize(Ldir + V);
806
+ // Anisotropic along the grain: a varnished board's highlight is a streak.
807
+ float aa = .34 * .34;
808
+ float D = D_GGXaniso(max(dot(Nw, H), 0.), dot(vec3(1.,0.,0.), H), dot(vec3(0.,1.,0.), H), aa * 4., aa);
809
+ vec3 F = F_Schlick(vec3(.045), max(dot(H, V), 0.));
810
+ vec3 wo = alb * (.16 + .95 * ndl);
811
+ wo += F * D * V_SmithGGX(ndv, ndl, aa) * ndl * 5.5 * uSpec;
812
+ wo += alb * pow(1. - ndv, 3.5) * .35;
813
+ plasma = toSrgb(tonemap(wo));
814
+ } else if (uMat < 5.5) {
815
+ // \u2500\u2500 stone \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
816
+ vec2 sq = warp(p * .022, .9);
817
+ float body = fbm(sq) * .62 + fbm(sq * 3.7) * .26 + fbm(sq * 11.) * .12;
818
+ float fleck = hash(floor(p * 2.2));
819
+ float vein = smoothstep(.52, .58, fbm(sq * .55 + 7.1));
820
+ float h0 = body + (fleck - .5) * .35;
821
+ float hx = fbm(warp((p + vec2(1.6, 0.)) * .022, .9)) + (hash(floor((p + vec2(1.6,0.)) * 2.2)) - .5) * .35;
822
+ float hy = fbm(warp((p + vec2(0., 1.6)) * .022, .9)) + (hash(floor((p + vec2(0.,1.6)) * 2.2)) - .5) * .35;
823
+ vec3 Ns = normalize(Nb + vec3((hx - h0) * 3.4, (hy - h0) * 3.4, 0.));
824
+
825
+ vec3 alb = mix(toLinear(vec3(.24, .235, .25)), toLinear(vec3(.55, .545, .55)), body);
826
+ alb = mix(alb, toLinear(vec3(.70, .69, .67)), vein * .5);
827
+ alb *= .92 + (fleck - .5) * .18;
828
+ alb = mix(alb, toLinear(tcol), talpha * .6);
829
+
830
+ float ndv = max(dot(Ns, V), 1e-3);
831
+ float ndl = max(dot(Ns, Ldir), 0.);
832
+ vec3 H = normalize(Ldir + V);
833
+ float aa = .72 * .72; // stone is rough
834
+ vec3 F = F_Schlick(vec3(.035), max(dot(H, V), 0.));
835
+ vec3 st = alb * (.20 + .95 * ndl);
836
+ st += F * D_GGX(max(dot(Ns, H), 0.), aa) * V_SmithGGX(ndv, ndl, aa) * ndl * 3.2 * uSpec;
837
+ st += alb * pow(1. - ndv, 4.) * .25; // dusty edge
838
+ plasma = toSrgb(tonemap(st));
839
+ } else {
840
+ // \u2500\u2500 cloud \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
841
+ // The one volume. The SDF is the boundary, density is noise inside it,
842
+ // and the light is marched rather than dotted: transmittance by
843
+ // Beer-Lambert, scattering by Henyey-Greenstein. A lambert cloud looks
844
+ // painted; a marched one looks lit from a direction.
845
+ // Bounded tight to the silhouette, which the edge displacement has
846
+ // already made billowy \u2014 the panel's outline is the cloud's outline. The
847
+ // interior is dense, so it reads as a thing made of cloud rather than as
848
+ // haze lying over a rectangle.
849
+ float inside = smoothstep(-1., 9., sd);
850
+ vec2 dq = warp(p * .0058 + vec2(uTime * .010, uTime * .004), 1.15);
851
+ float base = fbm(dq) * .58 + fbm(dq * 2.7) * .28 + fbm(dq * 6.9) * .14;
852
+ float dens = clamp(inside * (.55 + base * 1.15) - .12, 0., 1.);
853
+
854
+ // March toward the light and accumulate what it has to pass through.
855
+ float shadow = 0.;
856
+ vec2 step2 = normalize(Ldir.xy + vec2(1e-4)) * 16.;
857
+ for (int i = 1; i <= 5; i++) {
858
+ vec2 sp3 = p + step2 * float(i);
859
+ float ins = smoothstep(-1., 9., sd + float(i) * 5.);
860
+ vec2 q3 = warp(sp3 * .0058 + vec2(uTime * .010, uTime * .004), 1.15);
861
+ shadow += clamp(ins * (.55 + fbm(q3) * 1.15) - .12, 0., 1.);
862
+ }
863
+ float transmit = exp(-shadow * .55);
864
+ float cosT = dot(normalize(vec3(n, .6)), Ldir);
865
+ float g = .45;
866
+ float hg = (1. - g*g) / pow(1. + g*g - 2.*g*cosT, 1.5) * .0796;
867
+ vec3 sunCol = toLinear(vec3(1., .96, .89));
868
+ vec3 skyCol = toLinear(vec3(.42, .50, .64));
869
+ vec3 cl = skyCol * (.35 + .65 * dens) + sunCol * transmit * (.55 + 5.5 * hg);
870
+ cl = mix(cl, toLinear(tcol), talpha * .45);
871
+ plasma = toSrgb(tonemap(cl));
872
+ a = smoothstep(.04, .42, dens); // density is the coverage, not the outline
873
+ }
874
+
288
875
  col = mix(col, plasma, a);
289
876
  grain = 1. - a; // grain is background-only; panels stay clean
877
+ cover = a;
878
+ }
879
+ if (uClear > .5) {
880
+ // Outside a surface only its shadow lands on the page: black at the
881
+ // shadow's darkness. Inside, the surface itself. The edge blends the two
882
+ // by coverage, un-premultiplied for a non-premultiplied canvas.
883
+ float shade = clamp(shStr * smoothstep(.02, .55, hs) * (1. - uLight*.6), 0., 1.);
884
+ float alpha = max(cover, shade);
885
+ o = vec4(col * (cover / max(alpha, 1e-4)), alpha);
886
+ return;
290
887
  }
291
888
  // Keyed on viewport position, so a frame drawn for a taller region has the
292
889
  // same grain in its viewport band as the live frame that replaces it.
293
- col += (hash(p - uView.xy + uTime) - .5) * .025 * grain;
890
+ col += (hash(p - uView.xy + uTime) - .5) * .025 * grain * uGrain;
294
891
  o = vec4(col, 1.);
295
892
  }`;
296
893
  return { maskFrag, tintFrag, blurFrag, bgFrag, compFrag };
@@ -312,6 +909,14 @@ function hexToRgb(hex) {
312
909
  }
313
910
 
314
911
  // src/renderer.ts
912
+ var MATERIALS = ["plasma", "crystal", "metal", "mercury", "wood", "stone", "cloud"];
913
+ var FORMING_ATTR = "data-plasma-forming";
914
+ var FORMING_EVENT = "plasmaforming";
915
+ var FORMED_EVENT = "plasmaformed";
916
+ function dispatch(el, name, id) {
917
+ if (typeof CustomEvent === "undefined" || typeof el.dispatchEvent !== "function") return;
918
+ el.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: { id } }));
919
+ }
315
920
  var UNIFORMS = [
316
921
  "uRes",
317
922
  "uView",
@@ -361,7 +966,21 @@ var UNIFORMS = [
361
966
  "uRimColor",
362
967
  "uRimWidth",
363
968
  "uSpec",
364
- "uHair"
969
+ "uHair",
970
+ "uShim",
971
+ "uGlow",
972
+ "uWash",
973
+ "uGrain",
974
+ "uClear",
975
+ "uMat",
976
+ "uLightDir",
977
+ "uRough",
978
+ "uAniso",
979
+ "uEdge",
980
+ "uEdgeScale",
981
+ "uEdgeSharp",
982
+ "uThick",
983
+ "uTension"
365
984
  ];
366
985
  var MASK_SCALE = 0.5;
367
986
  var MAX_PIXELS = 26e5;
@@ -423,6 +1042,13 @@ var _PlasmaRenderer = class _PlasmaRenderer {
423
1042
  this.floatOK = false;
424
1043
  /** True between webglcontextlost and webglcontextrestored: draw nothing. */
425
1044
  this.lost = false;
1045
+ this.warnedOverflow = false;
1046
+ /**
1047
+ * Set by destroy(). The canvas and its context outlive this renderer, so an
1048
+ * async callback that lands afterwards would happily allocate on a context
1049
+ * nothing can free it from.
1050
+ */
1051
+ this.destroyed = false;
426
1052
  /** Everything initGL() created, so destroy() can free it by hand. */
427
1053
  this.owned = { tex: [], fb: [], prog: [], buf: [] };
428
1054
  this.recs = /* @__PURE__ */ new Map();
@@ -471,8 +1097,13 @@ var _PlasmaRenderer = class _PlasmaRenderer {
471
1097
  this.onContextRestored = () => {
472
1098
  this.lost = false;
473
1099
  this.initGL();
474
- this.applyResize();
475
1100
  this.last = 0;
1101
+ if (this.frozen) {
1102
+ clearTimeout(this.scrollIdle);
1103
+ this.thaw();
1104
+ return;
1105
+ }
1106
+ this.applyResize();
476
1107
  if (!document.hidden && !this.raf) this.raf = requestAnimationFrame(this.frame);
477
1108
  };
478
1109
  /**
@@ -608,6 +1239,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
608
1239
  r.formV += (170 * (1 - r.form) - 26 * r.formV) * dt;
609
1240
  r.form += r.formV * dt;
610
1241
  } else r.form = 1;
1242
+ if (r.forming && r.form > 0.985) {
1243
+ r.forming = false;
1244
+ r.el.removeAttribute(FORMING_ATTR);
1245
+ dispatch(r.el, FORMED_EVENT, r.id);
1246
+ }
611
1247
  const b = elementBox(r.el);
612
1248
  r.box = b;
613
1249
  if (b.w === 0 || b.l > right || b.t > bottom || b.l + b.w < left || b.t + b.h < top) {
@@ -704,6 +1340,10 @@ var _PlasmaRenderer = class _PlasmaRenderer {
704
1340
  r.drawn = { l, t, w: rgt - l, h: btm - t };
705
1341
  });
706
1342
  this.drawnScroll = [scrollX, scrollY];
1343
+ if (list.length > this.max && !this.warnedOverflow) {
1344
+ this.warnedOverflow = true;
1345
+ console.warn(`[plasma-ui] ${list.length} plasma surfaces are on screen but maxSurfaces is ${this.max}; the rest are not drawn. Raise maxSurfaces on <PlasmaProvider>.`);
1346
+ }
707
1347
  this.lastList = list.slice(0, this.max);
708
1348
  this.draw(this.lastList);
709
1349
  };
@@ -714,13 +1354,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
714
1354
  this.gl = gl;
715
1355
  this.settings = settings;
716
1356
  this.max = Math.max(1, Math.round(settings.maxSurfaces || DEFAULT_MAX_SHAPES));
717
- this.P = new Float32Array(this.max * 4);
718
- this.R = new Float32Array(this.max * 4);
719
- this.F = new Float32Array(this.max);
720
- this.T = new Float32Array(this.max * 4);
721
- this.FR = new Float32Array(this.max);
722
- this.EL = new Float32Array(this.max);
723
- this.SOLO = new Float32Array(this.max);
1357
+ this.allocUniformArrays();
724
1358
  this.initGL();
725
1359
  this.mouse.x = this.mouse.tx = innerWidth / 2;
726
1360
  this.mouse.y = this.mouse.ty = innerHeight / 2;
@@ -736,7 +1370,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
736
1370
  }
737
1371
  /** Returns null when WebGL2 is unavailable. */
738
1372
  static create(canvas, settings) {
739
- const gl = canvas.getContext("webgl2", { antialias: false, premultipliedAlpha: false });
1373
+ const gl = canvas.getContext("webgl2", {
1374
+ antialias: false,
1375
+ premultipliedAlpha: false,
1376
+ preserveDrawingBuffer: !!settings.preserveDrawingBuffer
1377
+ });
740
1378
  if (!gl) return null;
741
1379
  try {
742
1380
  return new _PlasmaRenderer(canvas, gl, settings);
@@ -771,7 +1409,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
771
1409
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
772
1410
  this.imgTex = tex;
773
1411
  const up = (source, w, h) => {
774
- if (!w || !h) return;
1412
+ if (this.destroyed || !w || !h) return;
775
1413
  gl.bindTexture(gl.TEXTURE_2D, this.imgTex);
776
1414
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
777
1415
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, source);
@@ -786,7 +1424,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
786
1424
  const img2 = src;
787
1425
  if (img2.complete && img2.naturalWidth) up(img2, img2.naturalWidth, img2.naturalHeight);
788
1426
  else img2.addEventListener("load", () => {
789
- if (this.imgSrc === src) up(img2, img2.naturalWidth, img2.naturalHeight);
1427
+ if (!this.destroyed && this.imgSrc === src) up(img2, img2.naturalWidth, img2.naturalHeight);
790
1428
  }, { once: true });
791
1429
  }
792
1430
  return;
@@ -800,7 +1438,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
800
1438
  const img = new Image();
801
1439
  img.crossOrigin = "anonymous";
802
1440
  img.onload = () => {
803
- if (this.imgSrc !== src) return;
1441
+ if (this.destroyed || this.imgSrc !== src) return;
804
1442
  const tex = gl.createTexture();
805
1443
  gl.bindTexture(gl.TEXTURE_2D, tex);
806
1444
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
@@ -818,7 +1456,12 @@ var _PlasmaRenderer = class _PlasmaRenderer {
818
1456
  }
819
1457
  configure(s, immediate = false) {
820
1458
  const qualityChanged = s.quality !== this.settings.quality;
1459
+ const max = Math.max(1, Math.round(s.maxSurfaces || DEFAULT_MAX_SHAPES));
821
1460
  this.settings = s;
1461
+ if (max !== this.max) {
1462
+ this.setMax(max);
1463
+ return;
1464
+ }
822
1465
  this.loadBackground(s.background ?? null);
823
1466
  this.colorTarget = s.colors.map(hexToRgb);
824
1467
  if (immediate) {
@@ -836,6 +1479,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
836
1479
  form: this.settings.reducedMotion ? 1 : 0,
837
1480
  formV: 0,
838
1481
  removing: false,
1482
+ forming: false,
839
1483
  lx: 0,
840
1484
  ly: 0,
841
1485
  leanCss: "",
@@ -853,6 +1497,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
853
1497
  drawn: null,
854
1498
  elevNow: -1
855
1499
  };
1500
+ if (!this.settings.reducedMotion && typeof el.setAttribute === "function") {
1501
+ rec.forming = true;
1502
+ el.setAttribute(FORMING_ATTR, "");
1503
+ dispatch(el, FORMING_EVENT, id);
1504
+ } else dispatch(el, FORMED_EVENT, id);
856
1505
  this.recs.set(id, rec);
857
1506
  return {
858
1507
  id,
@@ -868,16 +1517,22 @@ var _PlasmaRenderer = class _PlasmaRenderer {
868
1517
  remove: () => {
869
1518
  el.style.translate = "";
870
1519
  el.style.scale = "";
1520
+ if (rec.forming) el.removeAttribute(FORMING_ATTR);
871
1521
  this.recs.delete(id);
872
1522
  }
873
1523
  };
874
1524
  }
875
- /** Layout boxes of all shapes (lean removed; animating draggables report their destination). */
876
- layoutBoxes(excludeId, fusingOnly = false) {
1525
+ /**
1526
+ * Layout boxes of all shapes (lean removed; animating draggables report
1527
+ * their destination). Pass `group` to see only the surfaces in that group;
1528
+ * omit it - as any caller written before groups existed does - to see them all.
1529
+ */
1530
+ layoutBoxes(excludeId, fusingOnly = false, group) {
877
1531
  const out = [];
878
1532
  this.recs.forEach((r) => {
879
1533
  if (r.id === excludeId) return;
880
1534
  if (fusingOnly && r.fuse === false) return;
1535
+ if (group !== void 0 && (r.group ?? null) !== group) return;
881
1536
  out.push(this.layoutBoxOf(r));
882
1537
  });
883
1538
  return out;
@@ -903,6 +1558,39 @@ var _PlasmaRenderer = class _PlasmaRenderer {
903
1558
  bump(e) {
904
1559
  this.energy = Math.max(this.energy, Math.min(e, 1));
905
1560
  }
1561
+ /** Rebuild the shaders and uniform arrays for a new surface budget. */
1562
+ setMax(max) {
1563
+ this.max = max;
1564
+ this.allocUniformArrays();
1565
+ this.releaseGL();
1566
+ this.initGL();
1567
+ this.applyResize();
1568
+ this.warnedOverflow = false;
1569
+ }
1570
+ allocUniformArrays() {
1571
+ this.P = new Float32Array(this.max * 4);
1572
+ this.R = new Float32Array(this.max * 4);
1573
+ this.F = new Float32Array(this.max);
1574
+ this.T = new Float32Array(this.max * 4);
1575
+ this.FR = new Float32Array(this.max);
1576
+ this.EL = new Float32Array(this.max);
1577
+ this.SOLO = new Float32Array(this.max);
1578
+ }
1579
+ /** Delete every GL object this renderer created, leaving the canvas usable. */
1580
+ releaseGL() {
1581
+ const gl = this.gl;
1582
+ if (!gl.isContextLost()) {
1583
+ this.owned.tex.forEach((t) => gl.deleteTexture(t));
1584
+ this.owned.fb.forEach((f) => gl.deleteFramebuffer(f));
1585
+ this.owned.prog.forEach((pr) => gl.deleteProgram(pr));
1586
+ this.owned.buf.forEach((b) => gl.deleteBuffer(b));
1587
+ if (this.imgTex) gl.deleteTexture(this.imgTex);
1588
+ }
1589
+ this.imgTex = null;
1590
+ this.imgSrc = null;
1591
+ this.srcEl = null;
1592
+ this.owned = { tex: [], fb: [], prog: [], buf: [] };
1593
+ }
906
1594
  /**
907
1595
  * Every GL object this renderer owns. A lost context invalidates all of
908
1596
  * them, so creation lives here rather than in the constructor: the restore
@@ -911,6 +1599,9 @@ var _PlasmaRenderer = class _PlasmaRenderer {
911
1599
  initGL() {
912
1600
  const gl = this.gl;
913
1601
  this.owned = { tex: [], fb: [], prog: [], buf: [] };
1602
+ this.imgTex = null;
1603
+ this.imgSrc = null;
1604
+ this.srcEl = null;
914
1605
  this.floatOK = !!gl.getExtension("EXT_color_buffer_float");
915
1606
  const sh = makeShaders(this.max);
916
1607
  this.progs = { bg: this.program(sh.bgFrag), mask: this.program(sh.maskFrag), tint: this.program(sh.tintFrag), blur: this.program(sh.blurFrag), comp: this.program(sh.compFrag) };
@@ -934,6 +1625,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
934
1625
  this.canvas.width = this.canvas.height = 0;
935
1626
  }
936
1627
  destroy() {
1628
+ this.destroyed = true;
937
1629
  cancelAnimationFrame(this.raf);
938
1630
  clearTimeout(this.resizeSettle);
939
1631
  this.canvas.removeEventListener("webglcontextlost", this.onContextLost);
@@ -949,14 +1641,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
949
1641
  r.el.style.scale = "";
950
1642
  });
951
1643
  this.recs.clear();
952
- const gl = this.gl;
953
- if (!gl.isContextLost()) {
954
- this.owned.tex.forEach((t) => gl.deleteTexture(t));
955
- this.owned.fb.forEach((f) => gl.deleteFramebuffer(f));
956
- this.owned.prog.forEach((pr) => gl.deleteProgram(pr));
957
- this.owned.buf.forEach((b) => gl.deleteBuffer(b));
958
- }
959
- this.owned = { tex: [], fb: [], prog: [], buf: [] };
1644
+ this.releaseGL();
960
1645
  }
961
1646
  freeze() {
962
1647
  const c = this.canvas;
@@ -1033,6 +1718,8 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1033
1718
  gl.uniform3fv(u.uA, this.colors[0]);
1034
1719
  gl.uniform3fv(u.uB, this.colors[1]);
1035
1720
  gl.uniform3fv(u.uC, this.colors[2]);
1721
+ gl.uniform1f(u.uTension, s.tension);
1722
+ gl.uniform1f(u.uThick, s.thickness);
1036
1723
  };
1037
1724
  const bl = this.progs.blur;
1038
1725
  const pass = (src, dst, dx, dy) => {
@@ -1068,12 +1755,25 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1068
1755
  const bc = this.bgColor;
1069
1756
  gl.uniform3f(this.progs.bg.u.uBgColor, bc?.[0] ?? 0, bc?.[1] ?? 0, bc?.[2] ?? 0);
1070
1757
  gl.uniform1f(this.progs.bg.u.uBgSolid, bc ? 1 : 0);
1758
+ gl.uniform1f(this.progs.bg.u.uClear, s.ground === "clear" ? 1 : 0);
1071
1759
  gl.drawArrays(gl.TRIANGLES, 0, 3);
1072
1760
  gl.activeTexture(gl.TEXTURE0);
1073
1761
  gl.viewport(0, 0, this.rtA.w, this.rtA.h);
1074
1762
  gl.useProgram(bl.pr);
1075
1763
  gl.uniform1i(bl.u.uTex, 0);
1076
1764
  gl.activeTexture(gl.TEXTURE0);
1765
+ const bgBlur = Math.min(40, Math.max(0, s.backgroundBlur || 0));
1766
+ if (bgBlur > 0) {
1767
+ const step = bgBlur * this.dpr * MASK_SCALE / 4;
1768
+ pass(this.rtBg, this.rtBgM, 0, 0);
1769
+ for (let i = 0; i < 3; i++) {
1770
+ pass(this.rtBgM, this.rtB, step, 0);
1771
+ pass(this.rtB, this.rtBgM, 0, step);
1772
+ }
1773
+ gl.viewport(0, 0, this.rtBg.w, this.rtBg.h);
1774
+ pass(this.rtBgM, this.rtBg, 0, 0);
1775
+ gl.viewport(0, 0, this.rtA.w, this.rtA.h);
1776
+ }
1077
1777
  const n = list.length;
1078
1778
  let hasFrost = false, hasTint = false, hasElev = false;
1079
1779
  for (let i = 0; i < n; i++) {
@@ -1081,7 +1781,8 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1081
1781
  if (this.T[i * 4 + 3] > 2e-3) hasTint = true;
1082
1782
  if (this.EL[i] > 2e-3) hasElev = true;
1083
1783
  }
1084
- if (hasFrost) {
1784
+ const reflective = s.material === "metal" || s.material === "crystal";
1785
+ if (hasFrost || reflective) {
1085
1786
  pass(this.rtBg, this.rtB, 1.5 * k, 0);
1086
1787
  pass(this.rtB, this.rtBgM, 0, 2.5 * k);
1087
1788
  pass(this.rtBgM, this.rtB, 2.5 * k, 0);
@@ -1140,6 +1841,20 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1140
1841
  gl.uniform1f(c.u.uRimWidth, s.rimWidth);
1141
1842
  gl.uniform1f(c.u.uSpec, s.highlight);
1142
1843
  gl.uniform1f(c.u.uHair, s.edgeLine);
1844
+ gl.uniform1f(c.u.uShim, s.shimmer);
1845
+ gl.uniform1f(c.u.uGlow, s.glow);
1846
+ gl.uniform1f(c.u.uWash, s.wash);
1847
+ gl.uniform1f(c.u.uGrain, s.grain);
1848
+ gl.uniform1f(c.u.uClear, s.ground === "clear" ? 1 : 0);
1849
+ gl.uniform1f(c.u.uMat, Math.max(0, MATERIALS.indexOf(s.material)));
1850
+ gl.uniform3f(c.u.uLightDir, s.lightDir[0], s.lightDir[1], s.lightDir[2]);
1851
+ gl.uniform1f(c.u.uRough, s.roughness);
1852
+ gl.uniform1f(c.u.uAniso, s.anisotropy);
1853
+ gl.uniform1f(c.u.uEdge, s.edge);
1854
+ gl.uniform1f(c.u.uEdgeScale, s.edgeScale);
1855
+ gl.uniform1f(c.u.uEdgeSharp, s.edgeSharpness);
1856
+ gl.uniform1f(c.u.uThick, s.thickness);
1857
+ gl.uniform1f(c.u.uTension, s.tension);
1143
1858
  gl.activeTexture(gl.TEXTURE0);
1144
1859
  gl.bindTexture(gl.TEXTURE_2D, this.rtC.tex);
1145
1860
  gl.activeTexture(gl.TEXTURE1);
@@ -1186,15 +1901,40 @@ var _PlasmaRenderer = class _PlasmaRenderer {
1186
1901
  const sh = gl.createShader(type);
1187
1902
  gl.shaderSource(sh, src);
1188
1903
  gl.compileShader(sh);
1189
- if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh) || "shader compile failed");
1904
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
1905
+ const log = gl.getShaderInfoLog(sh);
1906
+ gl.deleteShader(sh);
1907
+ throw new Error(log || "shader compile failed");
1908
+ }
1190
1909
  return sh;
1191
1910
  };
1192
1911
  const pr = gl.createProgram();
1193
- gl.attachShader(pr, compile(gl.VERTEX_SHADER, vert));
1194
- gl.attachShader(pr, compile(gl.FRAGMENT_SHADER, fsrc));
1195
- gl.bindAttribLocation(pr, 0, "p");
1196
- gl.linkProgram(pr);
1197
- if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(pr) || "program link failed");
1912
+ const attached = [];
1913
+ try {
1914
+ const vs = compile(gl.VERTEX_SHADER, vert);
1915
+ let fs;
1916
+ try {
1917
+ fs = compile(gl.FRAGMENT_SHADER, fsrc);
1918
+ } catch (e) {
1919
+ gl.deleteShader(vs);
1920
+ throw e;
1921
+ }
1922
+ gl.attachShader(pr, vs);
1923
+ attached.push(vs);
1924
+ gl.attachShader(pr, fs);
1925
+ attached.push(fs);
1926
+ gl.bindAttribLocation(pr, 0, "p");
1927
+ gl.linkProgram(pr);
1928
+ if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(pr) || "program link failed");
1929
+ } catch (e) {
1930
+ gl.deleteProgram(pr);
1931
+ throw e;
1932
+ } finally {
1933
+ attached.forEach((sh) => {
1934
+ gl.detachShader(pr, sh);
1935
+ gl.deleteShader(sh);
1936
+ });
1937
+ }
1198
1938
  const u = {};
1199
1939
  UNIFORMS.forEach((n) => {
1200
1940
  u[n] = gl.getUniformLocation(pr, n);
@@ -1223,21 +1963,35 @@ var PlasmaRenderer = _PlasmaRenderer;
1223
1963
  import { jsx, jsxs } from "react/jsx-runtime";
1224
1964
  var noop = () => {
1225
1965
  };
1226
- var PlasmaContext = createContext({
1227
- renderer: null,
1966
+ var DEFAULT_RUNTIME = { renderer: null, supported: false, reducedMotion: false, pulse: noop, bump: noop };
1967
+ var DEFAULT_DEFAULTS = {
1228
1968
  tint: "#ffffff",
1229
1969
  opacity: 0,
1230
1970
  frost: 0,
1231
1971
  radius: 26,
1232
- supported: false,
1233
1972
  grid: 24,
1234
1973
  magnet: 40,
1235
- spring: { stiffness: 170, damping: 16 },
1236
- reducedMotion: false,
1237
- pulse: noop,
1238
- bump: noop
1239
- });
1240
- var usePlasma = () => useContext(PlasmaContext);
1974
+ spring: { stiffness: 170, damping: 16 }
1975
+ };
1976
+ var RuntimeContext = createContext(DEFAULT_RUNTIME);
1977
+ var DefaultsContext = createContext(DEFAULT_DEFAULTS);
1978
+ var AttachContext = createContext(null);
1979
+ var usePlasmaRuntime = () => useContext(RuntimeContext);
1980
+ var usePlasmaDefaults = () => useContext(DefaultsContext);
1981
+ function usePlasma() {
1982
+ const runtime = usePlasmaRuntime();
1983
+ const defaults = usePlasmaDefaults();
1984
+ return useMemo(() => ({ ...runtime, ...defaults }), [runtime, defaults]);
1985
+ }
1986
+ var DEV = typeof process !== "undefined" && true;
1987
+ var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
1988
+ function useLatest(value) {
1989
+ const ref = useRef(value);
1990
+ useIsoLayoutEffect(() => {
1991
+ ref.current = value;
1992
+ });
1993
+ return ref;
1994
+ }
1241
1995
  var FALLBACK_CSS = `
1242
1996
  .plasma-panel{box-sizing:border-box}
1243
1997
  .plasma-fallback{
@@ -1260,6 +2014,31 @@ function useReducedMotion() {
1260
2014
  }, []);
1261
2015
  return reduced;
1262
2016
  }
2017
+ function PlasmaCanvas({ className, style, zIndex = -1 }) {
2018
+ const attach = useContext(AttachContext);
2019
+ const { supported } = usePlasmaRuntime();
2020
+ useEffect(() => {
2021
+ if (DEV && !attach) console.warn("[plasma-ui] <PlasmaCanvas> must be rendered inside a <PlasmaProvider>.");
2022
+ }, [attach]);
2023
+ return /* @__PURE__ */ jsx(
2024
+ "canvas",
2025
+ {
2026
+ ref: attach ?? void 0,
2027
+ "aria-hidden": "true",
2028
+ className,
2029
+ style: {
2030
+ position: "fixed",
2031
+ inset: 0,
2032
+ width: "100%",
2033
+ height: "100%",
2034
+ zIndex,
2035
+ pointerEvents: "none",
2036
+ display: supported ? "block" : "none",
2037
+ ...style
2038
+ }
2039
+ }
2040
+ );
2041
+ }
1263
2042
  function PlasmaProvider({
1264
2043
  children,
1265
2044
  mood = "tidal",
@@ -1282,6 +2061,22 @@ function PlasmaProvider({
1282
2061
  rimWidth = 1,
1283
2062
  highlight = 1,
1284
2063
  edgeLine = 1,
2064
+ shimmer = 1,
2065
+ glow = 1,
2066
+ wash = 1,
2067
+ grain = 1,
2068
+ backgroundBlur = 0,
2069
+ ground = "field",
2070
+ preserveDrawingBuffer = false,
2071
+ material = "plasma",
2072
+ lightDir = [-0.42, -0.62, 0.66],
2073
+ roughness = 0.28,
2074
+ anisotropy = 0,
2075
+ edge = 0,
2076
+ edgeScale = 0.01,
2077
+ edgeSharpness = 0,
2078
+ thickness = 18,
2079
+ tension = 0,
1285
2080
  pointerDrop = true,
1286
2081
  ambientDrops = false,
1287
2082
  grid = 24,
@@ -1289,9 +2084,10 @@ function PlasmaProvider({
1289
2084
  quality = 1.25,
1290
2085
  maxSurfaces = 16,
1291
2086
  zIndex = -1,
1292
- freezeOnScroll = false
2087
+ freezeOnScroll = false,
2088
+ canvas = true
1293
2089
  }) {
1294
- const canvasRef = useRef(null);
2090
+ const [canvasEl, setCanvasEl] = useState(null);
1295
2091
  const [renderer, setRenderer] = useState(null);
1296
2092
  const [supported, setSupported] = useState(true);
1297
2093
  const reducedMotion = useReducedMotion();
@@ -1315,6 +2111,22 @@ function PlasmaProvider({
1315
2111
  rimWidth,
1316
2112
  highlight,
1317
2113
  edgeLine,
2114
+ shimmer,
2115
+ glow,
2116
+ wash,
2117
+ grain,
2118
+ backgroundBlur,
2119
+ ground,
2120
+ preserveDrawingBuffer,
2121
+ material,
2122
+ lightDir,
2123
+ roughness,
2124
+ anisotropy,
2125
+ edge,
2126
+ edgeScale,
2127
+ edgeSharpness,
2128
+ thickness,
2129
+ tension,
1318
2130
  viscosity,
1319
2131
  stretch,
1320
2132
  flow,
@@ -1323,91 +2135,55 @@ function PlasmaProvider({
1323
2135
  maxSurfaces,
1324
2136
  background: background ?? null
1325
2137
  };
1326
- const settingsRef = useRef(settings);
1327
- settingsRef.current = settings;
1328
- useLayoutEffect(() => {
1329
- const r = PlasmaRenderer.create(canvasRef.current, settingsRef.current);
2138
+ const settingsRef = useLatest(settings);
2139
+ useIsoLayoutEffect(() => {
2140
+ if (!canvasEl) return;
2141
+ const r = PlasmaRenderer.create(canvasEl, settingsRef.current);
1330
2142
  if (!r) {
1331
2143
  setSupported(false);
1332
2144
  return;
1333
2145
  }
2146
+ setSupported(true);
1334
2147
  setRenderer(r);
1335
2148
  return () => {
1336
2149
  r.destroy();
1337
2150
  setRenderer(null);
1338
2151
  };
1339
- }, []);
2152
+ }, [canvasEl]);
2153
+ useEffect(() => {
2154
+ if (!DEV || canvasEl) return;
2155
+ const t = setTimeout(() => console.warn("[plasma-ui] PlasmaProvider has no canvas. Either leave `canvas` on, or render a <PlasmaCanvas /> inside the provider."), 0);
2156
+ return () => clearTimeout(t);
2157
+ }, [canvasEl]);
2158
+ const settingsDeps = Object.values(settings).map((v) => Array.isArray(v) ? v.join() : v);
1340
2159
  useEffect(() => {
1341
2160
  renderer?.configure(settings);
1342
- }, [
1343
- renderer,
1344
- m.colors.join(),
1345
- settings.blend,
1346
- refraction,
1347
- dispersion,
1348
- rim,
1349
- smoothness,
1350
- settings.pointerDrop,
1351
- ambientDrops,
1352
- theme,
1353
- quality,
1354
- reducedMotion,
1355
- freezeOnScroll,
1356
- tint,
1357
- opacity,
1358
- rimColor,
1359
- rimWidth,
1360
- highlight,
1361
- edgeLine,
1362
- viscosity,
1363
- stretch,
1364
- flow,
1365
- frost,
1366
- elevation,
1367
- background
1368
- ]);
2161
+ }, [renderer, ...settingsDeps]);
1369
2162
  const vis = Math.min(Math.max(viscosity, 0), 1);
1370
2163
  const stiffK = vis < 0.5 ? 1.6 - 1.2 * vis : 1 - 1.1 * (vis - 0.5);
1371
2164
  const dampK = vis < 0.5 ? 0.6 + 0.8 * vis : 1 + 1.6 * (vis - 0.5);
1372
- const spring = { stiffness: m.spring.stiffness * stiffK, damping: m.spring.damping * dampK * Math.sqrt(stiffK) };
1373
- const value = useMemo(() => ({
1374
- renderer,
1375
- tint,
1376
- opacity,
1377
- frost,
1378
- radius,
1379
- supported,
1380
- grid,
1381
- magnet,
1382
- spring,
1383
- reducedMotion,
1384
- pulse: (x, y, s) => renderer?.pulse(x, y, s),
1385
- bump: (e) => renderer?.bump(e)
1386
- }), [renderer, tint, opacity, frost, radius, supported, grid, magnet, spring.stiffness, spring.damping, reducedMotion]);
1387
- return /* @__PURE__ */ jsxs(PlasmaContext.Provider, { value, children: [
2165
+ const stiffness = m.spring.stiffness * stiffK;
2166
+ const damping = m.spring.damping * dampK * Math.sqrt(stiffK);
2167
+ const rendererRef = useLatest(renderer);
2168
+ const pulse = useCallback((x, y, s) => rendererRef.current?.pulse(x, y, s), []);
2169
+ const bump = useCallback((e) => rendererRef.current?.bump(e), []);
2170
+ const runtime = useMemo(
2171
+ () => ({ renderer, supported, reducedMotion, pulse, bump }),
2172
+ [renderer, supported, reducedMotion, pulse, bump]
2173
+ );
2174
+ const defaults = useMemo(
2175
+ () => ({ tint, opacity, frost, radius, grid, magnet, spring: { stiffness, damping } }),
2176
+ [tint, opacity, frost, radius, grid, magnet, stiffness, damping]
2177
+ );
2178
+ return /* @__PURE__ */ jsx(RuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(DefaultsContext.Provider, { value: defaults, children: /* @__PURE__ */ jsxs(AttachContext.Provider, { value: setCanvasEl, children: [
1388
2179
  /* @__PURE__ */ jsx("style", { children: FALLBACK_CSS }),
1389
- /* @__PURE__ */ jsx(
1390
- "canvas",
1391
- {
1392
- ref: canvasRef,
1393
- "aria-hidden": "true",
1394
- style: {
1395
- position: "fixed",
1396
- inset: 0,
1397
- width: "100%",
1398
- height: "100%",
1399
- zIndex,
1400
- pointerEvents: "none",
1401
- display: supported ? "block" : "none"
1402
- }
1403
- }
1404
- ),
2180
+ canvas && /* @__PURE__ */ jsx(PlasmaCanvas, { zIndex }),
1405
2181
  children
1406
- ] });
2182
+ ] }) }) });
1407
2183
  }
1408
2184
 
1409
2185
  // src/Plasma.tsx
1410
- import { forwardRef, useCallback, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useRef as useRef2, useState as useState2 } from "react";
2186
+ import { forwardRef, useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
1411
2187
 
1412
2188
  // src/spring.ts
1413
2189
  var now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
@@ -1518,6 +2294,25 @@ function boxGap(a, b) {
1518
2294
  // src/Plasma.tsx
1519
2295
  import { jsx as jsx2 } from "react/jsx-runtime";
1520
2296
  var NO_DRAG = "button,a,input,textarea,select,label,[contenteditable],[data-plasma-nodrag]";
2297
+ var NO_SIDES = { top: false, right: false, bottom: false, left: false };
2298
+ var noSides = () => NO_SIDES;
2299
+ function createSidesStore() {
2300
+ let value = NO_SIDES;
2301
+ const subs = /* @__PURE__ */ new Set();
2302
+ return {
2303
+ subscribe(fn) {
2304
+ subs.add(fn);
2305
+ return () => {
2306
+ subs.delete(fn);
2307
+ };
2308
+ },
2309
+ get: () => value,
2310
+ set(next) {
2311
+ value = next;
2312
+ subs.forEach((fn) => fn());
2313
+ }
2314
+ };
2315
+ }
1521
2316
  function fallbackTint(hex, a) {
1522
2317
  if (!(a > 0) || !/^#([0-9a-f]{6})$/i.test(hex)) return null;
1523
2318
  const n = parseInt(hex.slice(1), 16);
@@ -1527,7 +2322,12 @@ function assignRef(ref, v) {
1527
2322
  if (typeof ref === "function") ref(v);
1528
2323
  else if (ref) ref.current = v;
1529
2324
  }
1530
- var Plasma = forwardRef(function Plasma2({
2325
+ function useConst(make) {
2326
+ const ref = useRef2(null);
2327
+ if (ref.current === null) ref.current = make();
2328
+ return ref.current;
2329
+ }
2330
+ var PlasmaInner = forwardRef(function Plasma({
1531
2331
  as: Comp = "div",
1532
2332
  radius,
1533
2333
  lean = 10,
@@ -1539,12 +2339,15 @@ var Plasma = forwardRef(function Plasma2({
1539
2339
  padding,
1540
2340
  draggable = false,
1541
2341
  snap = true,
2342
+ group,
1542
2343
  bounds,
1543
2344
  offset,
1544
2345
  defaultOffset,
1545
2346
  onDragStart,
1546
2347
  onDragEnd,
1547
2348
  onJoinChange,
2349
+ onForming,
2350
+ onFormed,
1548
2351
  className,
1549
2352
  style,
1550
2353
  children,
@@ -1553,29 +2356,51 @@ var Plasma = forwardRef(function Plasma2({
1553
2356
  tabIndex,
1554
2357
  ...rest
1555
2358
  }, ref) {
1556
- const plasma = usePlasma();
1557
- const r = radius ?? plasma.radius;
2359
+ const runtime = usePlasmaRuntime();
2360
+ const defaults = usePlasmaDefaults();
2361
+ const r = radius ?? defaults.radius;
1558
2362
  const el = useRef2(null);
1559
2363
  const handle = useRef2(null);
1560
- const [x] = useState2(() => springValue(offset?.x ?? defaultOffset?.x ?? 0));
1561
- const [y] = useState2(() => springValue(offset?.y ?? defaultOffset?.y ?? 0));
2364
+ const x = useConst(() => springValue(offset?.x ?? defaultOffset?.x ?? 0));
2365
+ const y = useConst(() => springValue(offset?.y ?? defaultOffset?.y ?? 0));
1562
2366
  const dest = useRef2({ x: x.get(), y: y.get() });
1563
2367
  const anims = useRef2([]);
1564
2368
  const [dragging, setDragging] = useState2(false);
1565
2369
  const positioned = draggable || !!offset || !!defaultOffset;
1566
- const joinCb = useRef2(onJoinChange);
1567
- joinCb.current = onJoinChange;
1568
- const [sides, setSides] = useState2({ top: false, right: false, bottom: false, left: false });
1569
- const plasmaRef = useRef2(plasma);
1570
- plasmaRef.current = plasma;
1571
- const setRef = useCallback((node) => {
2370
+ const joinCb = useLatest(onJoinChange);
2371
+ const formingCb = useLatest(onForming);
2372
+ const formedCb = useLatest(onFormed);
2373
+ const sidesStore = useConst(createSidesStore);
2374
+ const sides = useSyncExternalStore(sidesStore.subscribe, sidesStore.get, noSides);
2375
+ const runtimeRef = useLatest(runtime);
2376
+ const defaultsRef = useLatest(defaults);
2377
+ const boundsRef = useLatest(bounds);
2378
+ const snapRef = useLatest(snap);
2379
+ const onDragEndRef = useLatest(onDragEnd);
2380
+ const onDragStartRef = useLatest(onDragStart);
2381
+ const opts = {
2382
+ radius: r,
2383
+ lean: lean || 0,
2384
+ tint: tint ?? null,
2385
+ opacity: opacity ?? null,
2386
+ frost: frost ?? null,
2387
+ elevation: elevation ?? null,
2388
+ fuse,
2389
+ group: group ?? null
2390
+ };
2391
+ const optsRef = useLatest(opts);
2392
+ const setRef = useCallback2((node) => {
1572
2393
  el.current = node;
1573
2394
  assignRef(ref, node);
1574
2395
  }, [ref]);
1575
- useLayoutEffect2(() => {
1576
- const node = el.current, ren = plasma.renderer;
2396
+ useIsoLayoutEffect(() => {
2397
+ const node = el.current, ren = runtime.renderer;
1577
2398
  if (!node || !ren) return;
1578
- const h = ren.register(node, { radius: r, lean: lean || 0, tint: tint ?? null, opacity: opacity ?? null, frost: frost ?? null, elevation: elevation ?? null, fuse }, (j) => joinCb.current?.(j), setSides);
2399
+ const onForming2 = () => formingCb.current?.();
2400
+ const onFormed2 = () => formedCb.current?.();
2401
+ node.addEventListener(FORMING_EVENT, onForming2);
2402
+ node.addEventListener(FORMED_EVENT, onFormed2);
2403
+ const h = ren.register(node, optsRef.current, (j) => joinCb.current?.(j), sidesStore.set);
1579
2404
  if (positioned) {
1580
2405
  h.setLayoutBox(() => {
1581
2406
  const rect = node.getBoundingClientRect();
@@ -1593,12 +2418,14 @@ var Plasma = forwardRef(function Plasma2({
1593
2418
  return () => {
1594
2419
  h.remove();
1595
2420
  handle.current = null;
2421
+ node.removeEventListener(FORMING_EVENT, onForming2);
2422
+ node.removeEventListener(FORMED_EVENT, onFormed2);
1596
2423
  };
1597
- }, [plasma.renderer, positioned]);
1598
- useEffect2(() => {
1599
- handle.current?.update({ radius: r, lean: lean || 0, tint: tint ?? null, opacity: opacity ?? null, frost: frost ?? null, elevation: elevation ?? null, fuse });
1600
- }, [r, lean, tint, opacity, frost, elevation, fuse]);
1601
- useLayoutEffect2(() => {
2424
+ }, [runtime.renderer, positioned]);
2425
+ useIsoLayoutEffect(() => {
2426
+ handle.current?.update(opts);
2427
+ }, [r, lean, tint, opacity, frost, elevation, fuse, group]);
2428
+ useIsoLayoutEffect(() => {
1602
2429
  const node = el.current;
1603
2430
  if (!node || !positioned) return;
1604
2431
  const apply = () => {
@@ -1611,18 +2438,18 @@ var Plasma = forwardRef(function Plasma2({
1611
2438
  b();
1612
2439
  };
1613
2440
  }, [positioned]);
1614
- const springTo = useCallback((tx, ty, vx = 0, vy = 0) => {
2441
+ const springTo = useCallback2((tx, ty, vx = 0, vy = 0) => {
1615
2442
  dest.current = { x: tx, y: ty };
1616
2443
  anims.current.forEach((a) => a.stop());
1617
- const { spring, reducedMotion } = plasmaRef.current;
1618
- if (reducedMotion) {
2444
+ const { spring } = defaultsRef.current;
2445
+ if (runtimeRef.current.reducedMotion) {
1619
2446
  x.set(tx);
1620
2447
  y.set(ty);
1621
2448
  anims.current = [];
1622
2449
  return;
1623
2450
  }
1624
- const opts = { stiffness: spring.stiffness, damping: spring.damping };
1625
- anims.current = [animateSpring(x, tx, { ...opts, velocity: vx }), animateSpring(y, ty, { ...opts, velocity: vy })];
2451
+ const o = { stiffness: spring.stiffness, damping: spring.damping };
2452
+ anims.current = [animateSpring(x, tx, { ...o, velocity: vx }), animateSpring(y, ty, { ...o, velocity: vy })];
1626
2453
  }, []);
1627
2454
  useEffect2(() => {
1628
2455
  if (!offset) return;
@@ -1630,15 +2457,26 @@ var Plasma = forwardRef(function Plasma2({
1630
2457
  springTo(offset.x, offset.y);
1631
2458
  }, [offset?.x, offset?.y]);
1632
2459
  useEffect2(() => () => anims.current.forEach((a) => a.stop()), []);
1633
- const boundsBox = () => {
1634
- const b = bounds?.current?.getBoundingClientRect();
2460
+ useEffect2(() => {
2461
+ if (!DEV) return;
2462
+ if (!runtime.renderer && runtime.supported) {
2463
+ console.warn("[plasma-ui] <Plasma> rendered outside a <PlasmaProvider>: it will draw as the plain CSS fallback.");
2464
+ }
2465
+ const s = style;
2466
+ if (positioned && s && ("transform" in s || "translate" in s || "scale" in s)) {
2467
+ console.warn("[plasma-ui] <Plasma> owns transform, translate and scale on its element. A `style` that sets any of them will fight the drag and lean animations.");
2468
+ }
2469
+ }, [runtime.renderer, runtime.supported, positioned, style]);
2470
+ const boundsBox = useCallback2(() => {
2471
+ const b = boundsRef.current?.current?.getBoundingClientRect();
1635
2472
  return b ? { l: b.left, t: b.top, w: b.width, h: b.height } : { l: 0, t: 0, w: innerWidth, h: innerHeight };
1636
- };
1637
- const settle = (proposed, vx = 0, vy = 0) => {
2473
+ }, []);
2474
+ const settle = useCallback2((proposed, vx = 0, vy = 0) => {
1638
2475
  const node = el.current, h = handle.current;
1639
- const { grid, magnet, renderer } = plasmaRef.current;
2476
+ const { grid, magnet } = defaultsRef.current;
2477
+ const { renderer } = runtimeRef.current;
1640
2478
  let target = proposed;
1641
- if (snap && h && renderer) {
2479
+ if (snapRef.current && h && renderer) {
1642
2480
  const rect = node.getBoundingClientRect();
1643
2481
  const w = node.offsetWidth, hh = node.offsetHeight;
1644
2482
  const lo = h.leanOffset();
@@ -1646,15 +2484,15 @@ var Plasma = forwardRef(function Plasma2({
1646
2484
  const baseT = rect.top + (rect.height - hh) / 2 - lo.y - y.get();
1647
2485
  const s = snapBox(
1648
2486
  { l: baseL + proposed.x, t: baseT + proposed.y, w, h: hh },
1649
- renderer.layoutBoxes(h.id, true),
1650
- { grid, magnet, bounds: boundsBox(), inset: bounds ? 0 : 8 }
2487
+ renderer.layoutBoxes(h.id, true, optsRef.current.group ?? null),
2488
+ { grid, magnet, bounds: boundsBox(), inset: boundsRef.current ? 0 : 8 }
1651
2489
  );
1652
2490
  target = { x: s.l - baseL, y: s.t - baseT };
1653
2491
  }
1654
2492
  springTo(target.x, target.y, vx, vy);
1655
- onDragEnd?.(target);
1656
- };
1657
- const handlePointerDown = (e) => {
2493
+ onDragEndRef.current?.(target);
2494
+ }, []);
2495
+ const handlePointerDown = useCallback2((e) => {
1658
2496
  onPointerDown?.(e);
1659
2497
  if (!draggable || e.defaultPrevented || e.button !== 0) return;
1660
2498
  if (e.target.closest(NO_DRAG)) return;
@@ -1672,12 +2510,12 @@ var Plasma = forwardRef(function Plasma2({
1672
2510
  const clamp = (v, a, b) => Math.min(Math.max(v, a), Math.max(a, b));
1673
2511
  handle.current?.setDragging(true);
1674
2512
  setDragging(true);
1675
- onDragStart?.();
2513
+ onDragStartRef.current?.();
1676
2514
  const move = (ev) => {
1677
2515
  x.set(clamp(start.x + ev.clientX - start.px, minX - 40, maxX + 40));
1678
2516
  y.set(clamp(start.y + ev.clientY - start.py, minY - 40, maxY + 40));
1679
2517
  dest.current = { x: x.get(), y: y.get() };
1680
- plasmaRef.current.bump(Math.hypot(x.getVelocity(), y.getVelocity()) / 1800);
2518
+ runtimeRef.current.bump(Math.hypot(x.getVelocity(), y.getVelocity()) / 1800);
1681
2519
  };
1682
2520
  const up = () => {
1683
2521
  node.removeEventListener("pointermove", move);
@@ -1691,17 +2529,17 @@ var Plasma = forwardRef(function Plasma2({
1691
2529
  node.addEventListener("pointermove", move);
1692
2530
  node.addEventListener("pointerup", up);
1693
2531
  node.addEventListener("pointercancel", up);
1694
- };
1695
- const handleKeyDown = (e) => {
2532
+ }, [draggable, onPointerDown]);
2533
+ const handleKeyDown = useCallback2((e) => {
1696
2534
  onKeyDown?.(e);
1697
2535
  if (!draggable || e.defaultPrevented || e.target !== el.current) return;
1698
- const step = plasmaRef.current.grid || 24;
2536
+ const step = defaultsRef.current.grid || 24;
1699
2537
  const d = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] }[e.key];
1700
2538
  if (!d) return;
1701
2539
  e.preventDefault();
1702
2540
  settle({ x: dest.current.x + d[0], y: dest.current.y + d[1] });
1703
- };
1704
- const classes = ["plasma-panel", plasma.supported ? "" : "plasma-fallback", className].filter(Boolean).join(" ");
2541
+ }, [draggable, onKeyDown]);
2542
+ const classes = ["plasma-panel", runtime.supported ? "" : "plasma-fallback", className].filter(Boolean).join(" ");
1705
2543
  return /* @__PURE__ */ jsx2(
1706
2544
  Comp,
1707
2545
  {
@@ -1713,7 +2551,7 @@ var Plasma = forwardRef(function Plasma2({
1713
2551
  padding: `${sides.top ? padding / 2 : padding}px ${sides.right ? padding / 2 : padding}px ${sides.bottom ? padding / 2 : padding}px ${sides.left ? padding / 2 : padding}px`,
1714
2552
  transition: "padding 250ms ease"
1715
2553
  },
1716
- ...plasma.supported ? null : fallbackTint(tint ?? plasma.tint, opacity ?? plasma.opacity),
2554
+ ...runtime.supported ? null : fallbackTint(tint ?? defaults.tint, opacity ?? defaults.opacity),
1717
2555
  ...style
1718
2556
  },
1719
2557
  "data-plasma-draggable": draggable || void 0,
@@ -1726,9 +2564,15 @@ var Plasma = forwardRef(function Plasma2({
1726
2564
  }
1727
2565
  );
1728
2566
  });
2567
+ var Plasma2 = PlasmaInner;
1729
2568
  export {
1730
2569
  DEFAULT_MAX_SHAPES,
1731
- Plasma,
2570
+ FORMED_EVENT,
2571
+ FORMING_ATTR,
2572
+ FORMING_EVENT,
2573
+ MATERIALS,
2574
+ Plasma2 as Plasma,
2575
+ PlasmaCanvas,
1732
2576
  PlasmaProvider,
1733
2577
  PlasmaRenderer,
1734
2578
  boxGap,
@@ -1736,5 +2580,7 @@ export {
1736
2580
  moods,
1737
2581
  resolveMood,
1738
2582
  snapBox,
1739
- usePlasma
2583
+ usePlasma,
2584
+ usePlasmaDefaults,
2585
+ usePlasmaRuntime
1740
2586
  };