@cruxgarden/plasma-ui 0.2.0 → 0.2.3
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 +26 -0
- package/README.md +26 -50
- package/dist/PlasmaProvider.d.ts +8 -1
- package/dist/index.js +209 -45
- package/dist/renderer.d.ts +51 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
Fixes for the reports that came in after 0.2.0, and one new option.
|
|
6
|
+
|
|
7
|
+
- **Fixed:** the spring integrator diverged at small `stretch` with low
|
|
8
|
+
`viscosity` - stiffness grows as 1/stretch², and at the fixed substep
|
|
9
|
+
explicit Euler blew up, so the surface oscillated instead of settling. The
|
|
10
|
+
substep now follows the spring. Reachable from the playground: `stretch`
|
|
11
|
+
0.1 or 0.2 hit it.
|
|
12
|
+
- **Fixed:** lean rewrote `translate` on every panel every frame, forcing a
|
|
13
|
+
layout each frame. It now settles and writes only on change.
|
|
14
|
+
- **Fixed:** every `resize` event reallocated all render targets; a mobile
|
|
15
|
+
URL bar collapsing fired dozens per second. Coalesced to one per frame and
|
|
16
|
+
skipped when the pixel size is unchanged.
|
|
17
|
+
- **New:** `freezeOnScroll` on `PlasmaProvider` (off by default). On touch
|
|
18
|
+
devices, a fling pins a three-viewport frame to the page and pauses, so the
|
|
19
|
+
compositor scrolls it with the content; rendering resumes when the scroll
|
|
20
|
+
stops. Pinned frames match live ones to within one level.
|
|
21
|
+
- Passes that have nothing to do are skipped: the tint blur when no surface
|
|
22
|
+
has opacity, the frost blur when nothing is frosted or elevated, a copy
|
|
23
|
+
pass in the background chain. The loop pauses in a hidden tab. The
|
|
24
|
+
background target is 8-bit. Together, 20-40% fewer passes on clear looks;
|
|
25
|
+
pixel-identical output.
|
|
26
|
+
- The canvas is capped at 2.6M pixels, trading resolution rather than frame
|
|
27
|
+
rate on 4K displays and dense phones.
|
|
28
|
+
|
|
3
29
|
## 0.2.0
|
|
4
30
|
|
|
5
31
|
**Breaking:** the CSS class on every surface is `.plasma-panel`, was `.plasma-glass`.
|
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Plasma UI
|
|
2
2
|
|
|
3
|
-
Liquid panels for React.
|
|
3
|
+
Liquid panels for React, rendered in WebGL on canvas, inspired by Apple's Liquid Glass design. The `<Plasma>` panel looks and behaves like liquid, with surface tension that fuses on contact with other panels. Anything visible behind the panel is refracted. And for layout convenience, the panels ultimately snap to a grid layout. The library is a work in progress, extracted from the [Crux Garden](https://github.com/cruxgarden) project, but it seemed useful enough to share in its current form.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
|
-
[
|
|
7
|
+
[Playground and Docs](https://cruxgarden.github.io/plasma-ui/) · [Workspace example](https://cruxgarden.github.io/plasma-ui/examples/workspace/)
|
|
8
8
|
|
|
9
|
-
**Status: 0.
|
|
9
|
+
**Status: 0.2.0.** Core is stable and tested, but the API may change.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install @cruxgarden/plasma-ui
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Zero dependencies
|
|
15
|
+
Zero dependencies, except for React.
|
|
16
16
|
|
|
17
17
|
```tsx
|
|
18
18
|
import { PlasmaProvider, Plasma } from "@cruxgarden/plasma-ui";
|
|
@@ -33,31 +33,7 @@ export function App() {
|
|
|
33
33
|
|
|
34
34
|
## Example
|
|
35
35
|
|
|
36
|
-
[`examples/workspace`](examples/workspace)
|
|
37
|
-
|
|
38
|
-
## Not yet
|
|
39
|
-
|
|
40
|
-
Plasma UI 0.1 is a workspace library, not a full UI system. Know these before adopting:
|
|
41
|
-
|
|
42
|
-
- **No layers.** Overlapping surfaces fuse. Dialogs, menus, toasts, and fixed bars must be plain CSS for now (the docs site's own nav shows the pattern).
|
|
43
|
-
- **No clipping in scroll containers.** Plasma inside a scrollable list draws past its edges. Scrolling _inside_ one panel is fine.
|
|
44
|
-
- **No drag or resize handles.** `draggable` moves the whole surface; mark interactive children `data-plasma-nodrag`.
|
|
45
|
-
- **Draws its own background.** Browsers don't let WebGL read the rendered page, so the plasma refracts its background layer - the procedural mood field, or any color, image, canvas, or video you pass via `background` - rather than your live DOM. Custom background shaders aren't supported yet.
|
|
46
|
-
- **Rounded rectangles only.** No rotation or arbitrary shapes.
|
|
47
|
-
|
|
48
|
-
All of these are on the roadmap below.
|
|
49
|
-
|
|
50
|
-
## How it works
|
|
51
|
-
|
|
52
|
-
Markup stays ordinary HTML. `PlasmaProvider` renders one fixed canvas behind the page. Each frame:
|
|
53
|
-
|
|
54
|
-
1. **Silhouette.** Every registered element reports its box; one shader draws them as a single shape. Corners against a neighbor square off, and blending applies only where surfaces face different ways, so flush panels share a clean outline while gaps and steps get rounded fillets.
|
|
55
|
-
2. **Smoothing.** The silhouette is blurred and traced at its halfway contour, evening out curvature.
|
|
56
|
-
3. **Height.** A heavier blur becomes a height map; its slope drives refraction, so joined panels act as one lens.
|
|
57
|
-
4. **Tint and frost.** Color and translucency spread across the material with the same blur, so different values flow into each other across joins. The background renders once to a texture; two blurred copies serve the frosted plasma.
|
|
58
|
-
5. **Light.** The final pass refracts the background, splits color at the edges, and adds a rim and a pointer highlight.
|
|
59
|
-
|
|
60
|
-
Without WebGL2, `Plasma` falls back to a CSS frosted panel.
|
|
36
|
+
[`examples/workspace`](examples/workspace) demonstrates how the library can be used for a real-world dashboard layout.
|
|
61
37
|
|
|
62
38
|
## `<PlasmaProvider>`
|
|
63
39
|
|
|
@@ -91,7 +67,7 @@ Without WebGL2, `Plasma` falls back to a CSS frosted panel.
|
|
|
91
67
|
|
|
92
68
|
## `<Plasma>`
|
|
93
69
|
|
|
94
|
-
Accepts all HTML attributes plus:
|
|
70
|
+
Accepts all HTML attributes, plus the following:
|
|
95
71
|
|
|
96
72
|
| Prop | Type | Default | Description |
|
|
97
73
|
| --------------------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -126,19 +102,19 @@ const dusk: Mood = {
|
|
|
126
102
|
};
|
|
127
103
|
```
|
|
128
104
|
|
|
129
|
-
## Motion
|
|
105
|
+
## Motion
|
|
130
106
|
|
|
131
|
-
Each
|
|
107
|
+
Each panel undulates like a Slinky when moving. Three properties control this movement: viscosity, stretch, and flow.
|
|
132
108
|
|
|
133
109
|
```tsx
|
|
134
110
|
<PlasmaProvider viscosity={0.1} stretch={1.3} flow={0.6} /> // water
|
|
135
|
-
<PlasmaProvider viscosity={0.85} stretch={1.8} />
|
|
136
|
-
<PlasmaProvider stretch={0} />
|
|
111
|
+
<PlasmaProvider viscosity={0.85} stretch={1.8} /> // honey
|
|
112
|
+
<PlasmaProvider stretch={0} /> // the plasma tracks panels exactly
|
|
137
113
|
```
|
|
138
114
|
|
|
139
|
-
`flow` ripples the outline, so leave it at `0`
|
|
115
|
+
NOTE: `flow` ripples the outline, so leave it at `0` whenever flush edges should stay perfectly straight.
|
|
140
116
|
|
|
141
|
-
## Styling the rim
|
|
117
|
+
## Styling the rim (fancy outline)
|
|
142
118
|
|
|
143
119
|
```tsx
|
|
144
120
|
// solid cyan rim, a bit wider, no pointer highlight
|
|
@@ -155,27 +131,27 @@ Each surface is a spring chasing its element, and the drawn plasma always covers
|
|
|
155
131
|
|
|
156
132
|
## Guidelines
|
|
157
133
|
|
|
158
|
-
- Use
|
|
159
|
-
- Place surfaces
|
|
160
|
-
- Up to `maxSurfaces` (default 16) draw at once; offscreen
|
|
161
|
-
- Lean and
|
|
162
|
-
- `prefers-reduced-motion` disables
|
|
134
|
+
- Use Plasma for container components: panels, docks, cards, dialogs. Components should be nested inside.
|
|
135
|
+
- Place surfaces together or further apart than the Blend distance. Smaller gaps render as liquid bridging them.
|
|
136
|
+
- Up to `maxSurfaces` (default 16) draw at once; offscreen panels are skipped first. Two render passes loop over every slot per pixel, so set this value only as high as you need.
|
|
137
|
+
- Lean and Pulse use the CSS `translate` and `scale` properties, and Drag uses `transform`. Avoid setting these properties on `Plasma` elements yourself.
|
|
138
|
+
- `prefers-reduced-motion` disables Lean, Pulse, the pointer Drop, and Spring.
|
|
163
139
|
|
|
164
140
|
## Roadmap
|
|
165
141
|
|
|
166
|
-
|
|
142
|
+
Ordered by priority:
|
|
167
143
|
|
|
168
|
-
1. **Layers** -
|
|
169
|
-
2. **Drag handles and resize** - `handle` prop so panel content
|
|
144
|
+
1. **Layers** - panels that will stack instead of fusing. For use with dialogs, menus, and such.
|
|
145
|
+
2. **Drag handles and resize** - will add a `handle` prop for dragging, so panel content can be fully interactive. Also, edge resizing with grid snapping.
|
|
170
146
|
3. **Scroll clipping** - plasma confined to scrollable containers.
|
|
171
|
-
4. **Pluggable
|
|
172
|
-
5. **Shapes** -
|
|
147
|
+
4. **Pluggable Backgrounds** - colors, images, and live canvas/video shipped in 0.1 (`background` prop); custom shaders are next.
|
|
148
|
+
5. **Shapes and Orientation** - non-rectangular outlines, rotation...etc.
|
|
173
149
|
|
|
174
|
-
Contributions welcome
|
|
150
|
+
Contributions welcome for any of these - see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
175
151
|
|
|
176
152
|
## Browser support
|
|
177
153
|
|
|
178
|
-
Chrome, Edge, Firefox, and Safari 16.4+ (WebGL2).
|
|
154
|
+
Chrome, Edge, Firefox, and Safari 16.4+ (WebGL2). In non-supported browsers, `Plasma` renders as a CSS frosted panel and all layout, drag, and snap behavior still works.
|
|
179
155
|
|
|
180
156
|
## Development
|
|
181
157
|
|
|
@@ -202,7 +178,7 @@ does — see [PUBLISH.md](PUBLISH.md).
|
|
|
202
178
|
|
|
203
179
|
## Used by
|
|
204
180
|
|
|
205
|
-
- [Crux Garden](https://github.com/cruxgarden) - the
|
|
181
|
+
- [Crux Garden](https://github.com/cruxgarden) - the project Plasma UI was originally built for.
|
|
206
182
|
|
|
207
183
|
Using it in something? Add yours in a PR.
|
|
208
184
|
|
package/dist/PlasmaProvider.d.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface PlasmaProviderProps {
|
|
|
53
53
|
magnet?: number;
|
|
54
54
|
/** Maximum device pixel ratio for the canvas. Default 1.25. */
|
|
55
55
|
quality?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Touch devices only: during a fling, pin the last drawn frame to the page
|
|
58
|
+
* and pause, so the compositor scrolls it with the content; resume when the
|
|
59
|
+
* scrolling stops. Off by default. Needs the canvas's positioned ancestor,
|
|
60
|
+
* if any, to scroll with the page.
|
|
61
|
+
*/
|
|
62
|
+
freezeOnScroll?: boolean;
|
|
56
63
|
/** Maximum visible plasma surfaces. Compiled into the shaders, so it is fixed for the provider's lifetime; more surfaces cost GPU time. Default 16. */
|
|
57
64
|
maxSurfaces?: number;
|
|
58
65
|
/** z-index of the fixed canvas. Default -1 (behind content). */
|
|
@@ -76,4 +83,4 @@ export interface PlasmaContextValue {
|
|
|
76
83
|
bump: (energy: number) => void;
|
|
77
84
|
}
|
|
78
85
|
export declare const usePlasma: () => PlasmaContextValue;
|
|
79
|
-
export declare function PlasmaProvider({ children, mood, theme, blend, refraction, dispersion, rim, smoothness, background, radius, tint, opacity, frost, elevation, viscosity, stretch, flow, rimColor, rimWidth, highlight, edgeLine, pointerDrop, ambientDrops, grid, magnet, quality, maxSurfaces, zIndex, }: PlasmaProviderProps): React.JSX.Element;
|
|
86
|
+
export declare function PlasmaProvider({ children, mood, theme, blend, refraction, dispersion, rim, smoothness, background, radius, tint, opacity, frost, elevation, viscosity, stretch, flow, rimColor, rimWidth, highlight, edgeLine, pointerDrop, ambientDrops, grid, magnet, quality, maxSurfaces, zIndex, freezeOnScroll, }: PlasmaProviderProps): React.JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -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 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;
|
|
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;
|
|
@@ -61,7 +61,7 @@ float scene(vec2 p){
|
|
|
61
61
|
d = min(d, ds);
|
|
62
62
|
if(uAmbient > .5){
|
|
63
63
|
float t = uTime*.4;
|
|
64
|
-
vec2 home = vec2(
|
|
64
|
+
vec2 home = uView.xy + vec2(uView.z*.88, uView.w*.74);
|
|
65
65
|
d = smin(d, length(p - home - vec2(cos(t),sin(t*1.3))*70.) - 38., uGoo);
|
|
66
66
|
d = smin(d, length(p - home - vec2(cos(t*1.7+2.),sin(t*.9+1.))*90.) - 26., uGoo);
|
|
67
67
|
d = smin(d, length(p - home - vec2(sin(t*.7),cos(t*1.1))*40.) - 20., uGoo);
|
|
@@ -139,7 +139,7 @@ vec3 bg(vec2 p){
|
|
|
139
139
|
col = mix(col, uC, smoothstep(.55,.9, w.x*n*1.6));
|
|
140
140
|
float lines = abs(fract(n*14.)-.5);
|
|
141
141
|
col += (1.-smoothstep(0.,.06,lines)) * .07 * (0.6+uEnergy);
|
|
142
|
-
vec2 v = (p - vec2(0., uScroll))/
|
|
142
|
+
vec2 v = (p - vec2(0., uScroll) - uView.xy)/uView.zw - .5;
|
|
143
143
|
col *= .55 + .45*smoothstep(1.2,.2,length(v));
|
|
144
144
|
return mix(col, mix(vec3(.90,.93,.95), col, .42), uLight);
|
|
145
145
|
}
|
|
@@ -168,8 +168,8 @@ void main(){
|
|
|
168
168
|
+ vec2(fbm(q*.004 + uTime*.05) - .5, fbm(q*.004 + 31.7 - uTime*.04) - .5) * 22.)
|
|
169
169
|
* (.5 + uFlow*.5);
|
|
170
170
|
// cover-fit the image to the canvas
|
|
171
|
-
float sc = max(
|
|
172
|
-
vec2 uv = (q - .5*
|
|
171
|
+
float sc = max(uView.z / uImgRes.x, uView.w / uImgRes.y);
|
|
172
|
+
vec2 uv = (q - uView.xy - .5*uView.zw) / (uImgRes * sc) + .5;
|
|
173
173
|
o = vec4(texture(uImg, clamp(uv, 0., 1.)).rgb, 1.);
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
@@ -232,7 +232,10 @@ void main(){
|
|
|
232
232
|
float shStr = .5 * smoothstep(0., .12, el) * clamp(el + .35, 0., 1.);
|
|
233
233
|
vec3 col = back * (1. - shStr*smoothstep(.02, .55, hs)*(1.-uLight*.6));
|
|
234
234
|
float hHere = H(uv);
|
|
235
|
-
|
|
235
|
+
// Palette phases run on viewport position, like the grain: a frame drawn for
|
|
236
|
+
// a taller region must colour its viewport band exactly as the live frame does.
|
|
237
|
+
vec2 vp = p - uView.xy;
|
|
238
|
+
col += pal(uTime*.03 + vp.x/1400.) * .06 * smoothstep(.0, .45, hHere) * (1.+uEnergy);
|
|
236
239
|
|
|
237
240
|
if(sd > -2.){
|
|
238
241
|
vec2 th = 2. / vec2(textureSize(uH, 0));
|
|
@@ -277,7 +280,7 @@ void main(){
|
|
|
277
280
|
plasma += rimCol * fres * .45 * hl * uRim;
|
|
278
281
|
plasma += vec3(1.) * spec * .75 * hl * uSpec;
|
|
279
282
|
// faint shimmer across the body; fades out as the tint becomes opaque
|
|
280
|
-
plasma += pal(uTime*.04 +
|
|
283
|
+
plasma += pal(uTime*.04 + vp.y/900. + uEnergy*.3) * .05 * lift * (1.+uEnergy*2.) * hl * (1. - talpha);
|
|
281
284
|
vec3 hairCol = uRimMode > .5 ? mix(vec3(1.), rimCol / 1.4, .6) : vec3(.9,.95,1.);
|
|
282
285
|
plasma += hairCol * (1.-smoothstep(0., 1.6, abs(sd - .7))) * .4 * hl * uHair;
|
|
283
286
|
|
|
@@ -285,7 +288,9 @@ void main(){
|
|
|
285
288
|
col = mix(col, plasma, a);
|
|
286
289
|
grain = 1. - a; // grain is background-only; panels stay clean
|
|
287
290
|
}
|
|
288
|
-
|
|
291
|
+
// Keyed on viewport position, so a frame drawn for a taller region has the
|
|
292
|
+
// same grain in its viewport band as the live frame that replaces it.
|
|
293
|
+
col += (hash(p - uView.xy + uTime) - .5) * .025 * grain;
|
|
289
294
|
o = vec4(col, 1.);
|
|
290
295
|
}`;
|
|
291
296
|
return { maskFrag, tintFrag, blurFrag, bgFrag, compFrag };
|
|
@@ -309,6 +314,7 @@ function hexToRgb(hex) {
|
|
|
309
314
|
// src/renderer.ts
|
|
310
315
|
var UNIFORMS = [
|
|
311
316
|
"uRes",
|
|
317
|
+
"uView",
|
|
312
318
|
"uScale",
|
|
313
319
|
"uTime",
|
|
314
320
|
"uGoo",
|
|
@@ -358,6 +364,7 @@ var UNIFORMS = [
|
|
|
358
364
|
"uHair"
|
|
359
365
|
];
|
|
360
366
|
var MASK_SCALE = 0.5;
|
|
367
|
+
var MAX_PIXELS = 26e5;
|
|
361
368
|
function elementBox(el) {
|
|
362
369
|
const r = el.getBoundingClientRect();
|
|
363
370
|
const w = el.offsetWidth, h = el.offsetHeight;
|
|
@@ -405,7 +412,7 @@ function parseCssColor(src) {
|
|
|
405
412
|
if (r) return [+r[1] / 255, +r[2] / 255, +r[3] / 255];
|
|
406
413
|
return null;
|
|
407
414
|
}
|
|
408
|
-
var
|
|
415
|
+
var _PlasmaRenderer = class _PlasmaRenderer {
|
|
409
416
|
constructor(canvas, gl, settings) {
|
|
410
417
|
this.imgTex = null;
|
|
411
418
|
this.imgRes = [1, 1];
|
|
@@ -418,6 +425,21 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
418
425
|
this.last = 0;
|
|
419
426
|
this.time = 0;
|
|
420
427
|
this.dpr = 1;
|
|
428
|
+
this.resizeQueued = false;
|
|
429
|
+
/** Coarse pointer = touch. Only there does a fling run on the compositor with rAF deferred. */
|
|
430
|
+
this.coarse = typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
|
|
431
|
+
this.frozen = false;
|
|
432
|
+
this.resizeWhileFrozen = false;
|
|
433
|
+
/** Scroll offset the last frame was drawn for: where the pinned image belongs on the page. */
|
|
434
|
+
this.drawnScroll = [0, 0];
|
|
435
|
+
/**
|
|
436
|
+
* The region a frame covers, in CSS px: origin is where the viewport's
|
|
437
|
+
* top-left sits inside it, size is the whole region. Normally the viewport
|
|
438
|
+
* exactly. When pinning for a fling it is three viewports tall, so the
|
|
439
|
+
* scroll has a viewport of runway above and below before it runs out.
|
|
440
|
+
*/
|
|
441
|
+
this.region = { ox: 0, oy: 0, w: 0, h: 0 };
|
|
442
|
+
// viewports of runway above and below when pinned
|
|
421
443
|
this.energy = 0;
|
|
422
444
|
this.mouse = { x: 0, y: 0, tx: 0, ty: 0, amt: 0, target: 0 };
|
|
423
445
|
this.pulses = Array.from({ length: MAX_PULSES }, () => [0, 0, -99, 0]);
|
|
@@ -428,6 +450,53 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
428
450
|
this.lightQuery = typeof matchMedia !== "undefined" ? matchMedia("(prefers-color-scheme: light)") : null;
|
|
429
451
|
this.tintCache = /* @__PURE__ */ new Map();
|
|
430
452
|
this.RP = new Float32Array(MAX_PULSES * 4);
|
|
453
|
+
/**
|
|
454
|
+
* A hidden tab draws nothing anyone can see. Browsers throttle rAF there
|
|
455
|
+
* but do not all stop it, so the loop stops itself and picks up where it
|
|
456
|
+
* left off - `last` is reset so the first frame back does not see a
|
|
457
|
+
* minute-long dt.
|
|
458
|
+
*/
|
|
459
|
+
this.onVisibility = () => {
|
|
460
|
+
if (document.hidden) {
|
|
461
|
+
cancelAnimationFrame(this.raf);
|
|
462
|
+
this.raf = 0;
|
|
463
|
+
} else if (!this.raf && !this.frozen) {
|
|
464
|
+
this.last = 0;
|
|
465
|
+
this.raf = requestAnimationFrame(this.frame);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* A fling on a touch device is driven by the compositor, and rAF is deferred
|
|
470
|
+
* while it runs, so anything JS repositions arrives late: the plasma trails
|
|
471
|
+
* the panels it belongs to. Instead of chasing, the canvas is pinned to the
|
|
472
|
+
* page for the duration - the last drawn frame becomes a texture that
|
|
473
|
+
* scrolls with the content, moved by the compositor with no JS in the path -
|
|
474
|
+
* and the loop resumes once the scroll events stop.
|
|
475
|
+
*
|
|
476
|
+
* The field's scroll parallax is 1:1 on these devices (see draw), so the
|
|
477
|
+
* pinned image and the first live frame after it line up.
|
|
478
|
+
*/
|
|
479
|
+
this.onScroll = () => {
|
|
480
|
+
if (!this.settings.freezeOnScroll) return;
|
|
481
|
+
if (!this.frozen) this.freeze();
|
|
482
|
+
clearTimeout(this.scrollIdle);
|
|
483
|
+
this.scrollIdle = setTimeout(this.thaw, 120);
|
|
484
|
+
};
|
|
485
|
+
this.thaw = () => {
|
|
486
|
+
if (!this.frozen) return;
|
|
487
|
+
const c = this.canvas;
|
|
488
|
+
c.style.top = "";
|
|
489
|
+
c.style.left = "";
|
|
490
|
+
c.style.width = "100%";
|
|
491
|
+
c.style.height = "100%";
|
|
492
|
+
c.style.position = "fixed";
|
|
493
|
+
c.style.inset = "0";
|
|
494
|
+
this.frozen = false;
|
|
495
|
+
this.resizeWhileFrozen = false;
|
|
496
|
+
this.applyResize();
|
|
497
|
+
this.last = 0;
|
|
498
|
+
if (!document.hidden && !this.raf) this.raf = requestAnimationFrame(this.frame);
|
|
499
|
+
};
|
|
431
500
|
this.onPointer = (e) => {
|
|
432
501
|
this.mouse.tx = e.clientX;
|
|
433
502
|
this.mouse.ty = e.clientY;
|
|
@@ -436,24 +505,53 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
436
505
|
this.onLeave = () => {
|
|
437
506
|
this.mouse.target = 0;
|
|
438
507
|
};
|
|
508
|
+
/**
|
|
509
|
+
* Resizing reallocates eight render targets, so it must not run per event.
|
|
510
|
+
* Mobile fires `resize` continuously while the URL bar collapses, which
|
|
511
|
+
* otherwise reallocated every texture repeatedly mid-scroll.
|
|
512
|
+
*/
|
|
439
513
|
this.resize = () => {
|
|
514
|
+
if (this.resizeQueued) return;
|
|
515
|
+
this.resizeQueued = true;
|
|
516
|
+
requestAnimationFrame(() => {
|
|
517
|
+
this.resizeQueued = false;
|
|
518
|
+
this.applyResize();
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
this.applyResize = () => {
|
|
522
|
+
if (this.frozen) {
|
|
523
|
+
this.resizeWhileFrozen = true;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
this.region = { ox: 0, oy: 0, w: innerWidth, h: innerHeight };
|
|
527
|
+
this.allocate();
|
|
528
|
+
};
|
|
529
|
+
/** Size the canvas and every target for the current region. */
|
|
530
|
+
this.allocate = () => {
|
|
440
531
|
const gl = this.gl;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
532
|
+
const { w: rw, h: rh } = this.region;
|
|
533
|
+
let dpr = Math.min(devicePixelRatio || 1, this.settings.quality);
|
|
534
|
+
const pixels = rw * rh * dpr * dpr;
|
|
535
|
+
if (pixels > MAX_PIXELS) dpr *= Math.sqrt(MAX_PIXELS / pixels);
|
|
536
|
+
this.dpr = dpr;
|
|
537
|
+
const cw = Math.max(1, Math.round(rw * this.dpr));
|
|
538
|
+
const ch = Math.max(1, Math.round(rh * this.dpr));
|
|
539
|
+
if (cw === this.canvas.width && ch === this.canvas.height) return;
|
|
540
|
+
this.canvas.width = cw;
|
|
541
|
+
this.canvas.height = ch;
|
|
444
542
|
const w = Math.max(1, Math.round(this.canvas.width * MASK_SCALE));
|
|
445
543
|
const h = Math.max(1, Math.round(this.canvas.height * MASK_SCALE));
|
|
446
|
-
const size = (t, tw, th) => {
|
|
544
|
+
const size = (t, tw, th, color = false) => {
|
|
447
545
|
t.w = tw;
|
|
448
546
|
t.h = th;
|
|
449
547
|
gl.bindTexture(gl.TEXTURE_2D, t.tex);
|
|
450
|
-
if (this.floatOK) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, tw, th, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
548
|
+
if (this.floatOK && !color) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, tw, th, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
451
549
|
else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tw, th, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
452
550
|
gl.bindFramebuffer(gl.FRAMEBUFFER, t.fb);
|
|
453
551
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.tex, 0);
|
|
454
552
|
};
|
|
455
553
|
[this.rtA, this.rtB, this.rtC, this.rtT, this.rtFr, this.rtBgM, this.rtBgH].forEach((t) => size(t, w, h));
|
|
456
|
-
size(this.rtBg, this.canvas.width, this.canvas.height);
|
|
554
|
+
size(this.rtBg, this.canvas.width, this.canvas.height, true);
|
|
457
555
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.mrtFb);
|
|
458
556
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.rtT.tex, 0);
|
|
459
557
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.rtFr.tex, 0);
|
|
@@ -461,7 +559,7 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
461
559
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
462
560
|
};
|
|
463
561
|
this.frame = (now2) => {
|
|
464
|
-
this.raf = requestAnimationFrame(this.frame);
|
|
562
|
+
if (!this.oneShot) this.raf = requestAnimationFrame(this.frame);
|
|
465
563
|
const dt = this.last ? Math.min((now2 - this.last) / 1e3, 0.05) : 0.016;
|
|
466
564
|
this.last = now2;
|
|
467
565
|
const s = this.settings;
|
|
@@ -476,17 +574,18 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
476
574
|
}));
|
|
477
575
|
this.blend += (s.blend - this.blend) * ce;
|
|
478
576
|
this.energy *= Math.exp(-dt * 1.2);
|
|
479
|
-
const
|
|
577
|
+
const rg = this.region;
|
|
578
|
+
const top = -rg.oy - 80, bottom = rg.h - rg.oy + 80, left = -rg.ox - 80, right = rg.w - rg.ox + 80;
|
|
480
579
|
const list = [];
|
|
481
580
|
this.recs.forEach((r) => {
|
|
482
581
|
if (!r.el.isConnected) return;
|
|
483
582
|
if (!s.reducedMotion) {
|
|
484
|
-
r.formV += (170 * (1 - r.form) -
|
|
583
|
+
r.formV += (170 * (1 - r.form) - 26 * r.formV) * dt;
|
|
485
584
|
r.form += r.formV * dt;
|
|
486
585
|
} else r.form = 1;
|
|
487
586
|
const b = elementBox(r.el);
|
|
488
587
|
r.box = b;
|
|
489
|
-
if (b.w === 0 || b.l >
|
|
588
|
+
if (b.w === 0 || b.l > right || b.t > bottom || b.l + b.w < left || b.t + b.h < top) {
|
|
490
589
|
r.sp.live = false;
|
|
491
590
|
return;
|
|
492
591
|
}
|
|
@@ -532,10 +631,21 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
532
631
|
const le = 1 - Math.exp(-dt * 3);
|
|
533
632
|
r.lx += (tx - r.lx) * le;
|
|
534
633
|
r.ly += (ty - r.ly) * le;
|
|
535
|
-
|
|
634
|
+
if (Math.abs(tx - r.lx) < 0.05 && Math.abs(ty - r.ly) < 0.05) {
|
|
635
|
+
r.lx = tx;
|
|
636
|
+
r.ly = ty;
|
|
637
|
+
}
|
|
638
|
+
const lean = tx === 0 && ty === 0 && r.lx === 0 && r.ly === 0 ? "" : `${r.lx.toFixed(2)}px ${r.ly.toFixed(2)}px`;
|
|
639
|
+
if (lean !== r.leanCss) {
|
|
640
|
+
r.leanCss = lean;
|
|
641
|
+
r.el.style.translate = lean;
|
|
642
|
+
}
|
|
536
643
|
const u = (this.time - r.pulseAt) / 0.36;
|
|
537
|
-
|
|
538
|
-
|
|
644
|
+
const scale = r.pulseAt >= 0 && u >= 0 && u <= 1 && !s.reducedMotion ? String(1 + 0.04 * r.pulseS * Math.sin(Math.PI * u)) : "";
|
|
645
|
+
if (scale !== r.scaleCss) {
|
|
646
|
+
r.scaleCss = scale;
|
|
647
|
+
r.el.style.scale = scale;
|
|
648
|
+
}
|
|
539
649
|
});
|
|
540
650
|
const v = Math.min(Math.max(s.viscosity, 0), 1);
|
|
541
651
|
const st = Math.max(s.stretch, 0);
|
|
@@ -543,7 +653,8 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
543
653
|
const zeta = 0.28 + 0.87 * v;
|
|
544
654
|
const damp = 2 * zeta * Math.sqrt(stiff);
|
|
545
655
|
const sx = scrollX, sy = scrollY;
|
|
546
|
-
const
|
|
656
|
+
const hMax = 0.2 / Math.sqrt(Math.max(stiff, 1e-6));
|
|
657
|
+
const steps = Math.min(240, Math.max(Math.ceil(dt * 120), Math.ceil(dt / hMax), 1));
|
|
547
658
|
const h = dt / steps;
|
|
548
659
|
list.forEach((r) => {
|
|
549
660
|
const rr = r.el.getBoundingClientRect();
|
|
@@ -567,8 +678,13 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
567
678
|
const rgt = Math.max(tgt[2], sp.e[2]) - sx, btm = Math.max(tgt[3], sp.e[3]) - sy;
|
|
568
679
|
r.drawn = { l, t, w: rgt - l, h: btm - t };
|
|
569
680
|
});
|
|
570
|
-
this.
|
|
681
|
+
this.drawnScroll = [scrollX, scrollY];
|
|
682
|
+
this.lastList = list.slice(0, this.max);
|
|
683
|
+
this.draw(this.lastList);
|
|
571
684
|
};
|
|
685
|
+
this.lastList = [];
|
|
686
|
+
this.oneShot = false;
|
|
687
|
+
this.rgbCache = /* @__PURE__ */ new Map();
|
|
572
688
|
this.canvas = canvas;
|
|
573
689
|
this.gl = gl;
|
|
574
690
|
this.settings = settings;
|
|
@@ -601,9 +717,11 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
601
717
|
this.mouse.x = this.mouse.tx = innerWidth / 2;
|
|
602
718
|
this.mouse.y = this.mouse.ty = innerHeight / 2;
|
|
603
719
|
addEventListener("resize", this.resize);
|
|
720
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
721
|
+
if (this.coarse) addEventListener("scroll", this.onScroll, { passive: true });
|
|
604
722
|
addEventListener("pointermove", this.onPointer, { passive: true });
|
|
605
723
|
document.addEventListener("pointerleave", this.onLeave);
|
|
606
|
-
this.
|
|
724
|
+
this.applyResize();
|
|
607
725
|
this.raf = requestAnimationFrame(this.frame);
|
|
608
726
|
}
|
|
609
727
|
/** Returns null when WebGL2 is unavailable. */
|
|
@@ -710,6 +828,8 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
710
828
|
removing: false,
|
|
711
829
|
lx: 0,
|
|
712
830
|
ly: 0,
|
|
831
|
+
leanCss: "",
|
|
832
|
+
scaleCss: "",
|
|
713
833
|
joined: false,
|
|
714
834
|
dragging: false,
|
|
715
835
|
layoutBox: null,
|
|
@@ -754,7 +874,7 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
754
874
|
}
|
|
755
875
|
layoutBoxOf(r) {
|
|
756
876
|
if (r.layoutBox) return r.layoutBox();
|
|
757
|
-
const b = elementBox(r.el);
|
|
877
|
+
const b = r.box ?? elementBox(r.el);
|
|
758
878
|
return { l: b.l - r.lx, t: b.t - r.ly, w: b.w, h: b.h };
|
|
759
879
|
}
|
|
760
880
|
/** Send a pulse through the material from a viewport point. */
|
|
@@ -776,6 +896,9 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
776
896
|
destroy() {
|
|
777
897
|
cancelAnimationFrame(this.raf);
|
|
778
898
|
removeEventListener("resize", this.resize);
|
|
899
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
900
|
+
removeEventListener("scroll", this.onScroll);
|
|
901
|
+
clearTimeout(this.scrollIdle);
|
|
779
902
|
removeEventListener("pointermove", this.onPointer);
|
|
780
903
|
document.removeEventListener("pointerleave", this.onLeave);
|
|
781
904
|
this.recs.forEach((r) => {
|
|
@@ -785,6 +908,27 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
785
908
|
this.recs.clear();
|
|
786
909
|
this.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
787
910
|
}
|
|
911
|
+
freeze() {
|
|
912
|
+
const c = this.canvas;
|
|
913
|
+
cancelAnimationFrame(this.raf);
|
|
914
|
+
this.raf = 0;
|
|
915
|
+
const vw = innerWidth, vh = innerHeight, runway = Math.round(vh * _PlasmaRenderer.RUNWAY);
|
|
916
|
+
this.region = { ox: 0, oy: runway, w: vw, h: vh + 2 * runway };
|
|
917
|
+
this.allocate();
|
|
918
|
+
this.oneShot = true;
|
|
919
|
+
this.frame(performance.now());
|
|
920
|
+
this.oneShot = false;
|
|
921
|
+
c.style.inset = "";
|
|
922
|
+
c.style.width = `${this.region.w}px`;
|
|
923
|
+
c.style.height = `${this.region.h}px`;
|
|
924
|
+
c.style.position = "absolute";
|
|
925
|
+
c.style.top = "0px";
|
|
926
|
+
c.style.left = "0px";
|
|
927
|
+
const r = c.getBoundingClientRect();
|
|
928
|
+
c.style.top = `${-r.top - (scrollY - this.drawnScroll[1]) - runway}px`;
|
|
929
|
+
c.style.left = `${-r.left - (scrollX - this.drawnScroll[0])}px`;
|
|
930
|
+
this.frozen = true;
|
|
931
|
+
}
|
|
788
932
|
isLight() {
|
|
789
933
|
const t = this.settings.theme;
|
|
790
934
|
if (t !== "auto") return t === "light";
|
|
@@ -793,11 +937,11 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
793
937
|
return !!this.lightQuery?.matches;
|
|
794
938
|
}
|
|
795
939
|
draw(list) {
|
|
796
|
-
const gl = this.gl, s = this.settings;
|
|
940
|
+
const gl = this.gl, s = this.settings, rg = this.region;
|
|
797
941
|
const drawn = list.map((r) => r.drawn ?? elementBox(r.el));
|
|
798
942
|
list.forEach((r, i) => {
|
|
799
943
|
const b = drawn[i];
|
|
800
|
-
this.P.set([b.l + b.w / 2, b.t + b.h / 2, b.w / 2, b.h / 2], i * 4);
|
|
944
|
+
this.P.set([b.l + b.w / 2 + rg.ox, b.t + b.h / 2 + rg.oy, b.w / 2, b.h / 2], i * 4);
|
|
801
945
|
const squareAgainst = r.fuse === false ? [] : drawn.filter((_, j) => j !== i && list[j].fuse !== false);
|
|
802
946
|
this.R.set(cornerRadii(b, r.radius, squareAgainst, -1), i * 4);
|
|
803
947
|
this.F[i] = Math.max(0, r.form);
|
|
@@ -810,10 +954,11 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
810
954
|
this.EL[i] = r.elevNow;
|
|
811
955
|
this.SOLO[i] = r.fuse === false ? 1 : 0;
|
|
812
956
|
});
|
|
813
|
-
this.pulses.forEach((p, i) => this.RP.set(p, i * 4));
|
|
957
|
+
this.pulses.forEach((p, i) => this.RP.set([p[0] + rg.ox, p[1] + rg.oy, p[2], p[3]], i * 4));
|
|
814
958
|
const light = this.isLight() ? 1 : 0;
|
|
815
959
|
const setCommon = (u, scale) => {
|
|
816
|
-
gl.uniform2f(u.uRes,
|
|
960
|
+
gl.uniform2f(u.uRes, rg.w, rg.h);
|
|
961
|
+
gl.uniform4f(u.uView, rg.ox, rg.oy, innerWidth, innerHeight);
|
|
817
962
|
gl.uniform1f(u.uScale, scale);
|
|
818
963
|
gl.uniform1f(u.uTime, this.time);
|
|
819
964
|
gl.uniform1f(u.uGoo, this.blend);
|
|
@@ -822,10 +967,10 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
822
967
|
gl.uniform1f(u.uMouseAmt, this.mouse.amt);
|
|
823
968
|
gl.uniform1f(u.uDropR, s.pointerDrop ? 15 : 0);
|
|
824
969
|
gl.uniform1f(u.uAmbient, s.ambientDrops ? 1 : 0);
|
|
825
|
-
gl.uniform1f(u.uScroll, scrollY * 0.25);
|
|
970
|
+
gl.uniform1f(u.uScroll, this.coarse && s.freezeOnScroll ? scrollY - rg.oy : scrollY * 0.25);
|
|
826
971
|
gl.uniform1f(u.uVisc, Math.min(Math.max(s.viscosity, 0), 1));
|
|
827
972
|
gl.uniform1f(u.uFlow, Math.max(s.flow, 0));
|
|
828
|
-
gl.uniform2f(u.uMouse, this.mouse.x, this.mouse.y);
|
|
973
|
+
gl.uniform2f(u.uMouse, this.mouse.x + rg.ox, this.mouse.y + rg.oy);
|
|
829
974
|
gl.uniform4fv(u.uP, this.P);
|
|
830
975
|
gl.uniform4fv(u.uR, this.R);
|
|
831
976
|
gl.uniform1fv(u.uF, this.F);
|
|
@@ -879,14 +1024,20 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
879
1024
|
gl.useProgram(bl.pr);
|
|
880
1025
|
gl.uniform1i(bl.u.uTex, 0);
|
|
881
1026
|
gl.activeTexture(gl.TEXTURE0);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1027
|
+
const n = list.length;
|
|
1028
|
+
let hasFrost = false, hasTint = false, hasElev = false;
|
|
1029
|
+
for (let i = 0; i < n; i++) {
|
|
1030
|
+
if (this.FR[i] > 2e-3) hasFrost = true;
|
|
1031
|
+
if (this.T[i * 4 + 3] > 2e-3) hasTint = true;
|
|
1032
|
+
if (this.EL[i] > 2e-3) hasElev = true;
|
|
1033
|
+
}
|
|
1034
|
+
if (hasFrost) {
|
|
1035
|
+
pass(this.rtBg, this.rtB, 1.5 * k, 0);
|
|
1036
|
+
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
1037
|
+
pass(this.rtBgM, this.rtB, 2.5 * k, 0);
|
|
1038
|
+
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
1039
|
+
pass(this.rtBgM, this.rtB, 2.5 * k, 0);
|
|
1040
|
+
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
890
1041
|
pass(this.rtBgM, this.rtB, 5 * k, 0);
|
|
891
1042
|
pass(this.rtB, this.rtBgH, 0, 5 * k);
|
|
892
1043
|
pass(this.rtBgH, this.rtB, 5 * k, 0);
|
|
@@ -907,21 +1058,21 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
907
1058
|
gl.uniform1i(bl.u.uTex, 0);
|
|
908
1059
|
gl.activeTexture(gl.TEXTURE0);
|
|
909
1060
|
const so = 1.4 * k * s.smoothness;
|
|
910
|
-
for (let
|
|
1061
|
+
for (let i = 0; i < 3; i++) {
|
|
911
1062
|
pass(this.rtA, this.rtB, so, 0);
|
|
912
1063
|
pass(this.rtB, this.rtA, 0, so);
|
|
913
1064
|
}
|
|
914
|
-
for (let
|
|
1065
|
+
if (hasTint) for (let i = 0; i < 3; i++) {
|
|
915
1066
|
pass(this.rtT, this.rtB, so, 0);
|
|
916
1067
|
pass(this.rtB, this.rtT, 0, so);
|
|
917
1068
|
}
|
|
918
|
-
for (let
|
|
1069
|
+
if (hasFrost || hasElev) for (let i = 0; i < 3; i++) {
|
|
919
1070
|
pass(this.rtFr, this.rtB, so, 0);
|
|
920
1071
|
pass(this.rtB, this.rtFr, 0, so);
|
|
921
1072
|
}
|
|
922
1073
|
pass(this.rtA, this.rtB, 2 * k, 0);
|
|
923
1074
|
pass(this.rtB, this.rtC, 0, 2 * k);
|
|
924
|
-
for (let
|
|
1075
|
+
for (let i = 0; i < 3; i++) {
|
|
925
1076
|
pass(this.rtC, this.rtB, 2 * k, 0);
|
|
926
1077
|
pass(this.rtB, this.rtC, 0, 2 * k);
|
|
927
1078
|
}
|
|
@@ -964,6 +1115,14 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
964
1115
|
gl.activeTexture(gl.TEXTURE0);
|
|
965
1116
|
}
|
|
966
1117
|
rgb(hex) {
|
|
1118
|
+
let v = this.rgbCache.get(hex);
|
|
1119
|
+
if (!v) {
|
|
1120
|
+
v = this.rgbParse(hex);
|
|
1121
|
+
this.rgbCache.set(hex, v);
|
|
1122
|
+
}
|
|
1123
|
+
return v;
|
|
1124
|
+
}
|
|
1125
|
+
rgbParse(hex) {
|
|
967
1126
|
let c = this.tintCache.get(hex);
|
|
968
1127
|
if (!c) {
|
|
969
1128
|
c = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex) ? hexToRgb(hex) : [1, 1, 1];
|
|
@@ -1003,6 +1162,8 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
1003
1162
|
return { tex, fb: gl.createFramebuffer(), w: 0, h: 0 };
|
|
1004
1163
|
}
|
|
1005
1164
|
};
|
|
1165
|
+
_PlasmaRenderer.RUNWAY = 1;
|
|
1166
|
+
var PlasmaRenderer = _PlasmaRenderer;
|
|
1006
1167
|
|
|
1007
1168
|
// src/PlasmaProvider.tsx
|
|
1008
1169
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1073,7 +1234,8 @@ function PlasmaProvider({
|
|
|
1073
1234
|
magnet = 40,
|
|
1074
1235
|
quality = 1.25,
|
|
1075
1236
|
maxSurfaces = 16,
|
|
1076
|
-
zIndex = -1
|
|
1237
|
+
zIndex = -1,
|
|
1238
|
+
freezeOnScroll = false
|
|
1077
1239
|
}) {
|
|
1078
1240
|
const canvasRef = useRef(null);
|
|
1079
1241
|
const [renderer, setRenderer] = useState(null);
|
|
@@ -1092,6 +1254,7 @@ function PlasmaProvider({
|
|
|
1092
1254
|
theme,
|
|
1093
1255
|
quality,
|
|
1094
1256
|
reducedMotion,
|
|
1257
|
+
freezeOnScroll,
|
|
1095
1258
|
tint,
|
|
1096
1259
|
opacity,
|
|
1097
1260
|
rimColor,
|
|
@@ -1135,6 +1298,7 @@ function PlasmaProvider({
|
|
|
1135
1298
|
theme,
|
|
1136
1299
|
quality,
|
|
1137
1300
|
reducedMotion,
|
|
1301
|
+
freezeOnScroll,
|
|
1138
1302
|
tint,
|
|
1139
1303
|
opacity,
|
|
1140
1304
|
rimColor,
|
package/dist/renderer.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RendererSettings {
|
|
|
11
11
|
theme: "auto" | "light" | "dark";
|
|
12
12
|
quality: number;
|
|
13
13
|
reducedMotion: boolean;
|
|
14
|
+
/** Touch devices: pin the last frame to the page during a fling and resume when it stops. */
|
|
15
|
+
freezeOnScroll: boolean;
|
|
14
16
|
/** Rim color: "iridescent", "tint" (each surface's tint), or a hex color. */
|
|
15
17
|
rimColor: string;
|
|
16
18
|
/** Rim width multiplier. */
|
|
@@ -98,6 +100,22 @@ export declare class PlasmaRenderer {
|
|
|
98
100
|
private last;
|
|
99
101
|
private time;
|
|
100
102
|
private dpr;
|
|
103
|
+
private resizeQueued;
|
|
104
|
+
/** Coarse pointer = touch. Only there does a fling run on the compositor with rAF deferred. */
|
|
105
|
+
private coarse;
|
|
106
|
+
private frozen;
|
|
107
|
+
private scrollIdle;
|
|
108
|
+
private resizeWhileFrozen;
|
|
109
|
+
/** Scroll offset the last frame was drawn for: where the pinned image belongs on the page. */
|
|
110
|
+
private drawnScroll;
|
|
111
|
+
/**
|
|
112
|
+
* The region a frame covers, in CSS px: origin is where the viewport's
|
|
113
|
+
* top-left sits inside it, size is the whole region. Normally the viewport
|
|
114
|
+
* exactly. When pinning for a fling it is three viewports tall, so the
|
|
115
|
+
* scroll has a viewport of runway above and below before it runs out.
|
|
116
|
+
*/
|
|
117
|
+
private region;
|
|
118
|
+
private static readonly RUNWAY;
|
|
101
119
|
private energy;
|
|
102
120
|
private mouse;
|
|
103
121
|
private pulses;
|
|
@@ -131,13 +149,46 @@ export declare class PlasmaRenderer {
|
|
|
131
149
|
/** Raise the material's energy (brightens contours and color); it decays on its own. */
|
|
132
150
|
bump(e: number): void;
|
|
133
151
|
destroy(): void;
|
|
152
|
+
/**
|
|
153
|
+
* A hidden tab draws nothing anyone can see. Browsers throttle rAF there
|
|
154
|
+
* but do not all stop it, so the loop stops itself and picks up where it
|
|
155
|
+
* left off - `last` is reset so the first frame back does not see a
|
|
156
|
+
* minute-long dt.
|
|
157
|
+
*/
|
|
158
|
+
private onVisibility;
|
|
159
|
+
/**
|
|
160
|
+
* A fling on a touch device is driven by the compositor, and rAF is deferred
|
|
161
|
+
* while it runs, so anything JS repositions arrives late: the plasma trails
|
|
162
|
+
* the panels it belongs to. Instead of chasing, the canvas is pinned to the
|
|
163
|
+
* page for the duration - the last drawn frame becomes a texture that
|
|
164
|
+
* scrolls with the content, moved by the compositor with no JS in the path -
|
|
165
|
+
* and the loop resumes once the scroll events stop.
|
|
166
|
+
*
|
|
167
|
+
* The field's scroll parallax is 1:1 on these devices (see draw), so the
|
|
168
|
+
* pinned image and the first live frame after it line up.
|
|
169
|
+
*/
|
|
170
|
+
private onScroll;
|
|
171
|
+
private freeze;
|
|
172
|
+
private thaw;
|
|
134
173
|
private onPointer;
|
|
135
174
|
private onLeave;
|
|
175
|
+
/**
|
|
176
|
+
* Resizing reallocates eight render targets, so it must not run per event.
|
|
177
|
+
* Mobile fires `resize` continuously while the URL bar collapses, which
|
|
178
|
+
* otherwise reallocated every texture repeatedly mid-scroll.
|
|
179
|
+
*/
|
|
136
180
|
private resize;
|
|
181
|
+
private applyResize;
|
|
182
|
+
/** Size the canvas and every target for the current region. */
|
|
183
|
+
private allocate;
|
|
137
184
|
private isLight;
|
|
138
185
|
private frame;
|
|
186
|
+
private lastList;
|
|
187
|
+
private oneShot;
|
|
139
188
|
private draw;
|
|
189
|
+
private rgbCache;
|
|
140
190
|
private rgb;
|
|
191
|
+
private rgbParse;
|
|
141
192
|
private program;
|
|
142
193
|
private target;
|
|
143
194
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxgarden/plasma-ui",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Liquid panels for React: every panel is one shared plasma - they fuse on contact, refract, and snap to a grid.",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Liquid panels for React, rendered in WebGL on canvas: every panel is one shared plasma - they fuse on contact, refract, and snap to a grid.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "esbuild src/index.ts --bundle --format=esm --jsx=automatic --target=es2020 --external:react --external:react-dom --outfile=dist/index.js && tsc",
|