@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/CHANGELOG.md +130 -0
- package/README.md +162 -9
- package/dist/Plasma.d.ts +35 -6
- package/dist/PlasmaProvider.d.ts +122 -13
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1011 -165
- package/dist/renderer.d.ts +85 -2
- package/package.json +7 -3
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
|
-
|
|
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.
|
|
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", {
|
|
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
|
-
/**
|
|
876
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
|
1227
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
var
|
|
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
|
|
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 =
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const r = PlasmaRenderer.create(
|
|
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
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
supported,
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
spring,
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1557
|
-
const
|
|
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
|
|
1561
|
-
const
|
|
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 =
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1569
|
-
const
|
|
1570
|
-
|
|
1571
|
-
const
|
|
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
|
-
|
|
1576
|
-
const node = el.current, ren =
|
|
2396
|
+
useIsoLayoutEffect(() => {
|
|
2397
|
+
const node = el.current, ren = runtime.renderer;
|
|
1577
2398
|
if (!node || !ren) return;
|
|
1578
|
-
const
|
|
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
|
-
}, [
|
|
1598
|
-
|
|
1599
|
-
handle.current?.update(
|
|
1600
|
-
}, [r, lean, tint, opacity, frost, elevation, fuse]);
|
|
1601
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1625
|
-
anims.current = [animateSpring(x, tx, { ...
|
|
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
|
-
|
|
1634
|
-
|
|
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
|
|
2476
|
+
const { grid, magnet } = defaultsRef.current;
|
|
2477
|
+
const { renderer } = runtimeRef.current;
|
|
1640
2478
|
let target = proposed;
|
|
1641
|
-
if (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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",
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
};
|