@cruxgarden/plasma-ui 0.2.0 → 0.2.2

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 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. Every `<Plasma>` panel joins one shared plasma: surfaces fuse on contact, refract what's behind them, and snap to a grid when dragged. Built for workspace and canvas UIs - tool panels, dashboards, launchers - by [Crux Garden](https://github.com/cruxgarden).
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
- ![Dragging a panel: it tears off its group, travels as liquid, fuses with another panel, and snaps to the grid](docs/demo.gif)
5
+ ![Five panels in a workspace: one is dragged out of its group and travels as liquid, another is dropped against a neighbour and fuses into it, and each snaps to the 24px grid](docs/demo.gif)
6
6
 
7
- [**Try it**](https://cruxgarden.github.io/plasma-ui/) · [workspace example](https://cruxgarden.github.io/plasma-ui/examples/workspace/)
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.1.0.** The core material is stable and tested; the API may change between minor versions before 1.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 beyond React.
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) is a small working app - inbox, reader, tasks, and a player as draggable fused panels, with focus-driven elevation and layout persisted to localStorage. `node examples/workspace/build.mjs` builds it to a single html file.
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 feel
105
+ ## Motion
130
106
 
131
- Each surface is a spring chasing its element, and the drawn plasma always covers the element. Moving panels leave a trailing stretch; stopping ones overshoot before settling. Scrolling doesn't count as motion.
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} /> // honey
136
- <PlasmaProvider stretch={0} /> // the plasma tracks panels exactly
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` where flush edges should stay perfectly straight.
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 plasma for containers: panels, docks, cards, dialogs. Small controls read better as regular HTML on top.
159
- - Place surfaces either flush (they become one piece) or further apart than the blend distance. Smaller gaps render as liquid bridging.
160
- - Up to `maxSurfaces` (default 16) draw at once; offscreen ones are skipped first. Two render passes loop over every slot per pixel, so raise it only as far as you need.
161
- - Lean and pulses use the CSS `translate` and `scale` properties, and drag uses `transform`, so they compose with each other. Avoid setting those on `Plasma` elements yourself.
162
- - `prefers-reduced-motion` disables lean, pulses, the pointer drop, and springs.
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
- In priority order. Not a schedule.
142
+ Ordered by priority:
167
143
 
168
- 1. **Layers** - independent materials that stack instead of fusing, for dialogs, menus, and fixed chrome over plasma.
169
- 2. **Drag handles and resize** - `handle` prop so panel content stays fully interactive; edge resize with grid snapping.
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 backgrounds** - colors, images, and live canvas/video shipped in 0.1 (`background` prop); custom shaders next.
172
- 5. **Shapes** - rotation and non-rectangular outlines.
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 on any of these - see [CONTRIBUTING.md](CONTRIBUTING.md).
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). Elsewhere, `Plasma` renders as a CSS frosted panel and all layout, drag, and snap behavior still works.
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 workspace Plasma UI was built for.
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
 
@@ -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(uRes.x*.88, uRes.y*.74);
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))/uRes - .5;
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(uRes.x / uImgRes.x, uRes.y / uImgRes.y);
172
- vec2 uv = (q - .5*uRes) / (uImgRes * sc) + .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
- col += pal(uTime*.03 + p.x/1400.) * .06 * smoothstep(.0, .45, hHere) * (1.+uEnergy);
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 + p.y/900. + uEnergy*.3) * .05 * lift * (1.+uEnergy*2.) * hl * (1. - talpha);
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
- col += (hash(p + uTime) - .5) * .025 * grain;
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 PlasmaRenderer = class _PlasmaRenderer {
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
- this.dpr = Math.min(devicePixelRatio || 1, this.settings.quality);
442
- this.canvas.width = Math.max(1, Math.round(innerWidth * this.dpr));
443
- this.canvas.height = Math.max(1, Math.round(innerHeight * this.dpr));
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,7 +574,8 @@ 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 vw = innerWidth, vh = innerHeight;
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;
@@ -486,7 +585,7 @@ var PlasmaRenderer = class _PlasmaRenderer {
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 > vw + 80 || b.t > vh + 80 || b.l + b.w < -80 || b.t + b.h < -80) {
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
- r.el.style.translate = `${r.lx.toFixed(2)}px ${r.ly.toFixed(2)}px`;
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
- if (r.pulseAt >= 0 && u >= 0 && u <= 1 && !s.reducedMotion) r.el.style.scale = String(1 + 0.04 * r.pulseS * Math.sin(Math.PI * u));
538
- else if (r.el.style.scale) r.el.style.scale = "";
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 steps = Math.max(1, Math.ceil(dt * 120));
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.draw(list.slice(0, this.max));
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.resize();
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, innerWidth, innerHeight);
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
- if (list.some((_, i) => this.FR[i] > 2e-3)) {
883
- pass(this.rtBg, this.rtBgM, 1.5 * k, 0);
884
- pass(this.rtBgM, this.rtB, 0, 2.5 * k);
885
- pass(this.rtB, this.rtBgM, 2.5 * k, 0);
886
- pass(this.rtBgM, this.rtB, 0, 2.5 * k);
887
- pass(this.rtB, this.rtBgM, 2.5 * k, 0);
888
- pass(this.rtBgM, this.rtB, 0, 2.5 * k);
889
- pass(this.rtB, this.rtBgM, 0, 0);
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 n = 0; n < 3; n++) {
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 n = 0; n < 3; n++) {
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 n = 0; n < 3; n++) {
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 n = 0; n < 3; n++) {
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,
@@ -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.0",
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.2",
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",