@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 CHANGED
@@ -1,5 +1,135 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### A clear ground, so a second provider can sit above a scrim
6
+
7
+ The material was one canvas behind everything, so anything drawn above a
8
+ scrim - a dialog, a menu - could never be plasma. Two props make a second
9
+ provider possible on top of the first.
10
+
11
+ - **Added:** `ground` - `"field"` (the default, the old behaviour) paints the
12
+ background everywhere; `"clear"` leaves the canvas transparent outside the
13
+ surfaces, draws only the surfaces, their shadows and rims, and refracts the
14
+ `background` source sampled exactly (no swirl), so it lines up with what is
15
+ beneath. Alpha is the surface's coverage, the shadow's darkness outside it.
16
+ - **Added:** `preserveDrawingBuffer` - keep each frame after it is shown so
17
+ another provider can pass this canvas as its `background`. Fixed at
18
+ creation; off by default.
19
+ - **Added:** the form-in is observable. A surface's element carries
20
+ `data-plasma-forming` (`FORMING_ATTR`) from `register` until its form-in
21
+ has settled, and dispatches `plasmaforming` / `plasmaformed`
22
+ (`FORMING_EVENT` / `FORMED_EVENT`, bubbling); `<Plasma>` exposes them as
23
+ `onForming` / `onFormed`. Hold the contents back and reveal them in
24
+ `onFormed`, or style them off the attribute.
25
+ - The pair: the first provider with `preserveDrawingBuffer` and a
26
+ `<PlasmaCanvas className="ground" />`; the second, inside the dialog's own
27
+ stacking context, with `ground="clear"` and `background={groundCanvas}`,
28
+ its `<PlasmaCanvas>` between the scrim and the panel, and the panel
29
+ registered on it.
30
+
31
+ ### Every look the material adds is now a control
32
+
33
+ Six things gave the plasma a look of its own and only two could be turned off,
34
+ so "plain glass, nothing but the lens" was not reachable. Each is a prop now,
35
+ every one defaulting to exactly the old behaviour.
36
+
37
+ - **Added:** `shimmer` - the slow iridescent sheen drifting across the body of
38
+ each surface. This is the rainbow that stayed no matter how far `rim` came
39
+ down; it had no uniform at all.
40
+ - **Added:** `glow` - the halo of color the plasma casts on the background
41
+ around it. Keyed to the height field, not to elevation, which is why it was
42
+ still there at `elevation={0}` where the drop shadow genuinely is off.
43
+ - **Added:** `wash` - how much of its own cast the material puts on what you
44
+ see through it. `0` passes the background straight through.
45
+ - **Added:** `grain` - the film grain over the background.
46
+ - **Added:** `backgroundBlur` (0-40 px) - blurs the background itself, where
47
+ `frost` blurs only what a frosted surface sees. Eight extra passes above 0,
48
+ and none at 0. Contributed as a patch by Tigrana.
49
+ - **Added:** an **Aqua** tab in the playground: all six off, refraction up. The
50
+ README carries the same recipe under "Clear as water".
51
+
52
+ ### Composability
53
+
54
+ - **Added:** `<PlasmaCanvas>` and `canvas={false}` on the provider, so the
55
+ canvas element can be placed and styled by the consumer. The renderer still
56
+ draws the whole viewport - this moves the element, not the field.
57
+ - **Added:** `group` on `<Plasma>`. Two independent sets of panels on one page
58
+ no longer snap onto each other. Snapping only; surfaces still fuse wherever
59
+ they overlap.
60
+ - **Added:** `usePlasmaRuntime()` and `usePlasmaDefaults()`. The context is two
61
+ contexts now, and the runtime half is stable, so a component that only wants
62
+ `pulse` is no longer re-rendered by every styling change. `usePlasma()`
63
+ returns both together and is unchanged.
64
+ - **Changed:** `as` is properly polymorphic. `<Plasma as="a" href>` and
65
+ `<Plasma as={Link} to>` typecheck; `PlasmaProps` on its own still means the
66
+ div form.
67
+ - **Changed:** `maxSurfaces` is live. It recompiles the shaders rather than
68
+ being fixed at mount, and the renderer warns once when it has to drop
69
+ surfaces instead of silently not drawing them.
70
+ - **Changed:** `bounds` takes `RefObject<HTMLElement | null>`, which is what
71
+ `useRef<T>(null)` produces under @types/react 19, where it did not compile.
72
+ - **Changed:** `PlasmaRenderer`, `makeShaders` and the renderer types are
73
+ marked `@internal` - still exported as an escape hatch, but outside semver.
74
+
75
+ ### React
76
+
77
+ - **Fixed:** refs were written during render (`settingsRef`, `joinCb`,
78
+ `plasmaRef`). They update in a layout effect now.
79
+ - **Fixed:** a surface's options reached the renderer one paint late, because
80
+ the update ran in a passive effect while the CSS it accompanies commits with
81
+ layout. Registration also read those options through a closure its own dep
82
+ list could leave stale for a frame; it reads a ref instead.
83
+ - **Fixed:** `useLayoutEffect` warned during server rendering. Both components
84
+ use an isomorphic variant, and join state reads through
85
+ `useSyncExternalStore` with a server snapshot - SSR is clean, and tested.
86
+ - **Fixed:** interaction handlers were new identities on every render, which
87
+ broke memoization for `as={MemoizedComponent}`.
88
+ - **Added:** development-only warnings for a `<Plasma>` outside a provider, a
89
+ provider with no canvas, and a `style` setting `transform`, `translate` or
90
+ `scale` on a positioned surface - the three failures that were silent. They
91
+ drop out of a production build.
92
+
93
+ ### Gate
94
+
95
+ - `npm run verify` also runs `typecheck:app` (the site, the examples and a
96
+ compile-only API suite, none of which were typechecked before) and
97
+ `format:check`. CI adds `typecheck:react19`, since the peer range is
98
+ `react >=18` while the dev types pin 18.
99
+ - Prettier no longer claims `src`, `tests` or CSS, whose dense style is
100
+ deliberate; `npm run format` used to hand a 690-line diff to anyone who ran it.
101
+
102
+
103
+ Follow-ups to the context-loss work in 0.2.4, which fixed the frame loop but
104
+ left the paths around it holding objects from the dead context.
105
+
106
+ - **Fixed:** a `background` image was gone for good after a context restore.
107
+ `initGL()` re-ran `configure()`, but `loadBackground()` early-returns on an
108
+ unchanged source, so `imgTex` kept its handle from the dead context while
109
+ `uHasImg` still said 1 — every frame then threw INVALID_OPERATION on
110
+ `bindTexture`. The background state is reset before the reconfigure, so the
111
+ source is genuinely reloaded.
112
+ - **Fixed:** a restore that landed during a `freezeOnScroll` fling left the
113
+ canvas blank. `applyResize()` refuses to allocate while frozen, so the eight
114
+ targets got no storage and the multi-target framebuffer no attachments, and
115
+ the loop restarted into a freeze that was meant to hold it. A restore now
116
+ ends the freeze and reallocates.
117
+ - **Fixed:** `destroy()` leaked the background texture — created in
118
+ `loadBackground()`, so never in the owned set — and a full-size texture went
119
+ with every teardown, including each StrictMode remount.
120
+ - **Fixed:** a background image whose load landed after `destroy()` allocated
121
+ an orphan texture on the still-live shared context.
122
+ - **Fixed:** compiled shaders were never deleted. `deleteProgram()` does not
123
+ free a shader that was not flagged, so ten shader objects stranded per
124
+ `initGL()` — once per remount and once per context restore.
125
+ - **Changed:** the provider's `configure()` effect derives its dependencies
126
+ from the settings object rather than a hand-written list, so a new setting is
127
+ live the moment it is added instead of silently freezing at its mount value.
128
+ - **Added:** a fake WebGL2 + DOM harness (`tests/webgl-harness.mjs`) and seven
129
+ renderer lifecycle tests. It accounts for every GL object created and fails
130
+ on any use of one from a dead context or already deleted — the two mistakes
131
+ that are invisible in a browser until the field goes black.
132
+
3
133
  ## 0.2.4
4
134
 
5
135
  - **Fixed:** the field stopped rendering after a window resize and never came
package/README.md CHANGED
@@ -6,13 +6,14 @@ Liquid panels for React, rendered in WebGL on canvas, inspired by Apple's Liquid
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.2.0.** Core is stable and tested, but the API may change.
9
+ **Status: 0.3.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, except for React.
15
+ Zero dependencies, except for React. Best on desktop: the effect is GPU-heavy
16
+ and does not run well on mobile.
16
17
 
17
18
  ```tsx
18
19
  import { PlasmaProvider, Plasma } from "@cruxgarden/plasma-ui";
@@ -58,12 +59,32 @@ export function App() {
58
59
  | `rimWidth` | `number` | `1` | How far the rim reaches in from the edge |
59
60
  | `highlight` | `number` | `1` | Pointer-facing highlight; `0` turns it off |
60
61
  | `edgeLine` | `number` | `1` | Thin line along the outline; `0` turns it off |
62
+ | `shimmer` | `number` | `1` | The slow iridescent sheen that drifts across the body of each surface; `0` turns it off |
63
+ | `glow` | `number` | `1` | The halo of color the plasma casts on the background around it - the soft light that is still there at `elevation={0}`; `0` turns it off |
64
+ | `wash` | `number` | `1` | How much of its own cast the material puts on what you see through it; `0` passes the background straight through |
65
+ | `grain` | `number` | `1` | Film grain over the background (never over the surfaces); `0` turns it off |
66
+ | `backgroundBlur` | `number` | `0` | Blur the background itself, in CSS px (0-40). Unlike `frost`, which blurs only what a frosted surface sees, this softens the whole field |
67
+ | `ground` | `"field" \| "clear"` | `"field"` | What the canvas shows where there is no surface. `"clear"` leaves it transparent, so a second provider's canvas can sit above other content (a dialog above a scrim) and draw only its surfaces; pass the first provider's canvas as its `background` and the surfaces refract it |
68
+ | `preserveDrawingBuffer` | `boolean` | `false` | Keep each frame after it is shown so another provider can sample this canvas as its `background`. Fixed at creation |
69
+ While a surface forms in (about half a second, never under reduced motion) its element carries `data-plasma-forming` (`FORMING_ATTR`) and dispatches `plasmaforming` then `plasmaformed` (`FORMING_EVENT`, `FORMED_EVENT`; bubbling, `detail.id`). Style the children off the attribute, or listen for the events, to have the contents arrive after the material: `[data-plasma-forming] > * { opacity: 0 }` with a transition on opacity. `<Plasma>` wraps the events as `onForming` / `onFormed`; a surface registered by hand gets them on its element.
70
+
71
+ | `material` | `MaterialName` | `"plasma"` | What the surfaces are made of: `plasma`, `crystal`, `metal`, `wood`, `stone` or `cloud`. Every material shares the same geometry, springs and fusing and differs only in how it is shaded - see [`examples/materials`](examples/materials) |
72
+ | `lightDir` | `[number, number, number]` | up-left, front | Where the one light comes from. Every opaque material reads it, so two of them on a page agree about the sun |
73
+ | `roughness` | `number` | `0.28` | Surface finish for `metal`: 0 is a mirror, 1 is chalk |
74
+ | `anisotropy` | `number` | `0` | How far a highlight stretches along the grain. Brushed metal and varnished wood both want it |
75
+ | `edge` | `number` | `0` | How far the outline is displaced from its rounded box, in CSS px. A rounded rectangle is right for a liquid and wrong for most else: stone chips, cloud billows, cut metal does neither |
76
+ | `edgeScale` | `number` | `0.01` | Size of that displacement, in cycles per px: small is billows, large is chips |
77
+ | `edgeSharpness` | `number` | `0` | `0` rolls the displaced edge, `1` breaks it into flats and points |
78
+ | `thickness` | `number` | `18` | How thick a panel is **as a solid**, in CSS px. The marched materials light a body of this depth rather than shading a flat card |
79
+ | `tension` | `number` | `0` | Surface tension: how hard the material pulls its own shape toward a bead, and how eagerly two of them merge. `mercury` runs high |
61
80
  | `pointerDrop` | `boolean` | `true` | Liquid drop that follows the pointer |
62
81
  | `ambientDrops` | `boolean` | `false` | Decorative orbiting drops |
63
82
  | `grid`, `magnet` | `number` | `24`, `40` | Snap grid size and edge latch distance |
64
83
  | `quality` | `number` | `1.25` | Maximum canvas pixel ratio |
65
- | `maxSurfaces` | `number` | `16` | Visible surface budget, compiled into the shaders (fixed at mount); higher costs GPU time |
84
+ | `freezeOnScroll` | `boolean` | `false` | Touch devices only: pin the last drawn frame to the page through a fling and resume when the scrolling stops |
85
+ | `maxSurfaces` | `number` | `16` | Visible surface budget, compiled into the shaders; changing it rebuilds them, and higher costs GPU time |
66
86
  | `zIndex` | `number` | `-1` | Canvas stacking order |
87
+ | `canvas` | `boolean` | `true` | `false`: render [`<PlasmaCanvas />`](#plasmacanvas) yourself to choose where the element sits and how it is styled |
67
88
 
68
89
  ## `<Plasma>`
69
90
 
@@ -71,7 +92,7 @@ Accepts all HTML attributes, plus the following:
71
92
 
72
93
  | Prop | Type | Default | Description |
73
94
  | --------------------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- |
74
- | `as` | `ElementType` | `"div"` | Element to render |
95
+ | `as` | `ElementType` | `"div"` | Element or component to render. Its props typecheck through: `as="a"` takes `href`, `as={Link}` takes `to` |
75
96
  | `radius` | `number` | provider | Corner radius (px) for this surface |
76
97
  | `lean` | `number \| false` | `10` | Lean toward the pointer while standalone |
77
98
  | `tint`, `opacity`, `frost`, `elevation` | `string`, `number` | provider | Color, translucency, and shadow depth for this surface; joined surfaces with different values blend into each other |
@@ -80,15 +101,138 @@ Accepts all HTML attributes, plus the following:
80
101
  | `draggable` | `boolean` | `false` | Move freely, snap on release; arrow keys move one grid step |
81
102
  | `snap` | `boolean` | `true` | Latch to neighbor edges, otherwise the grid |
82
103
  | `bounds` | `RefObject<HTMLElement>` | viewport | Drag area and grid origin |
104
+ | `group` | `string` | | Snap only against surfaces in the same group; surfaces with no group form one group of their own |
83
105
  | `offset` / `defaultOffset` | `{ x, y }` | | Controlled or initial offset; changes spring into place |
84
106
  | `onDragStart`, `onDragEnd(offset)` | | | Drag lifecycle; `onDragEnd` gets the settled offset |
85
107
  | `onJoinChange(joined)` | | | Fires when the surface fuses with or separates from a neighbor |
108
+ | `onForming()`, `onFormed()` | | | The form-in: starts (not under reduced motion), and has settled (at once under reduced motion) — reveal the contents in `onFormed` |
109
+
110
+ Everything else you pass goes to the rendered element. `PlasmaProps<C>` is the
111
+ full prop type for `<Plasma as={C}>`; `PlasmaOwnProps` is just the table above,
112
+ if you need to wrap `Plasma` in a component of your own.
86
113
 
87
114
  Drag ignores presses on buttons, links, inputs, and anything marked `data-plasma-nodrag`.
88
115
 
89
- ## `usePlasma()`
116
+ ## Hooks
117
+
118
+ | Hook | Returns |
119
+ | --------------------- | ----------------------------------------------------------------------------- |
120
+ | `usePlasmaRuntime()` | `renderer`, `supported`, `reducedMotion`, `pulse(x, y, strength?)`, `bump(energy)` |
121
+ | `usePlasmaDefaults()` | `tint`, `opacity`, `frost`, `radius`, `grid`, `magnet`, `spring` |
122
+ | `usePlasma()` | both of the above, in one object |
123
+
124
+ `usePlasmaRuntime()` is the one to reach for when you only need `pulse`: its
125
+ value is stable, so a component reading it is not re-rendered every time a
126
+ styling prop on the provider changes. `usePlasma()` is the convenient one and
127
+ re-renders on any change.
128
+
129
+ ## `<PlasmaCanvas>`
130
+
131
+ The provider renders the canvas itself unless you pass `canvas={false}`, in
132
+ which case render `<PlasmaCanvas />` wherever you want the element to live:
133
+
134
+ ```tsx
135
+ <PlasmaProvider canvas={false}>
136
+ <div className="page-backdrop" />
137
+ <PlasmaCanvas zIndex={0} className="field" />
138
+ <main>...</main>
139
+ </PlasmaProvider>
140
+ ```
90
141
 
91
- Returns `pulse(x, y, strength?)`, `bump(energy)`, `supported`, `grid`, `magnet`, `spring`, and `reducedMotion`.
142
+ It takes `className`, `style` and `zIndex`. Note that this places and styles
143
+ the **element**; the renderer still draws the whole viewport. Confining the
144
+ field to a container is [roadmap](#roadmap) work, not something this prop does.
145
+
146
+ ## Above a scrim: a second canvas
147
+
148
+ The material is one canvas behind everything, so a dialog above a scrim, or a
149
+ menu above the pane it opened from, could never be plasma. Two props make a
150
+ second provider possible on top of the first. The ground keeps its frames
151
+ (`preserveDrawingBuffer`) and hands its canvas out; the overlay draws with a
152
+ clear ground (`ground="clear"`): transparent outside its surfaces, only the
153
+ surfaces, their shadows and rims drawn, and what they refract is the ground
154
+ canvas passed as `background`, sampled exactly so it lines up with the page.
155
+
156
+ ```tsx
157
+ // The ground, once, behind the app.
158
+ <PlasmaProvider mood="tidal" preserveDrawingBuffer canvas={false}>
159
+ <PlasmaCanvas zIndex={-1} className="ground" />
160
+ <App />
161
+ </PlasmaProvider>
162
+
163
+ // A dialog: its own provider inside its own stacking context, the canvas
164
+ // between the scrim and the panel, the panel registered on it.
165
+ function Dialog({ children }) {
166
+ const ground = document.querySelector("canvas.ground");
167
+ return (
168
+ <div className="dialog-root"> {/* position: fixed; inset: 0 */}
169
+ <div className="scrim" />
170
+ <PlasmaProvider ground="clear" background={ground} canvas={false}
171
+ pointerDrop={false} grain={0} glow={0} maxSurfaces={2}>
172
+ <PlasmaCanvas style={{ position: "absolute", zIndex: 1 }} />
173
+ <Plasma className="panel" elevation={0.7} style={{ zIndex: 2 }}>
174
+ {children}
175
+ </Plasma>
176
+ </PlasmaProvider>
177
+ </div>
178
+ );
179
+ }
180
+ ```
181
+
182
+ Each overlay is a full render pass while it is open, at the same size as the
183
+ ground, plus one texture upload of the ground canvas per frame. Mount it with
184
+ the dialog and unmount it with the dialog; nothing runs while none is open.
185
+ The page under the scrim is not in the sample - WebGL cannot read the DOM -
186
+ but the scrim has already dimmed it to the field. Give the panel no fill,
187
+ border or shadow of its own: the rim is its edge and the elevation its shadow.
188
+
189
+ ## Contents after the material
190
+
191
+ A surface forms in over about half a second when it registers. Content drawn
192
+ on top of it from the first frame looks as if it arrived before its panel,
193
+ so the form-in is observable: the element carries `data-plasma-forming`
194
+ (`FORMING_ATTR`) while it runs and dispatches `plasmaforming` then
195
+ `plasmaformed` (`FORMING_EVENT`, `FORMED_EVENT`; bubbling, `detail.id`).
196
+ Under reduced motion there is no form-in: no attribute, and `plasmaformed`
197
+ at once. `<Plasma>` wraps the events as `onForming` / `onFormed`.
198
+
199
+ ```css
200
+ /* hold the contents back, then fade them in once the material has arrived */
201
+ [data-plasma-forming] > * { opacity: 0; transition: none; }
202
+ .panel > * { transition: opacity 160ms ease-out; }
203
+ ```
204
+
205
+ ```tsx
206
+ <Plasma onFormed={() => setReady(true)}>{ready && <Contents />}</Plasma>
207
+ ```
208
+
209
+ ## Clear as water
210
+
211
+ Six things give the material a look of its own, and each is a separate
212
+ control, so "plain glass, nothing but the lens" is a configuration rather than
213
+ a fork:
214
+
215
+ ```tsx
216
+ <PlasmaProvider
217
+ rim={0} // the iridescent edge
218
+ highlight={0} // the specular that follows the pointer
219
+ shimmer={0} // the sheen drifting across the body
220
+ glow={0} // the halo cast on the background - visible even at elevation 0
221
+ wash={0} // the material's own tint on what you see through it
222
+ grain={0} // film grain on the background
223
+ edgeLine={0.35} // a hairline is usually still wanted: it is what reads as an edge
224
+ refraction={1.5}
225
+ dispersion={1.6}
226
+ />
227
+ ```
228
+
229
+ The **Aqua** tab in the [playground](https://cruxgarden.github.io/plasma-ui/)
230
+ is exactly this, with all six sliders live next to it.
231
+
232
+ Two of these answer questions that come up often: the faint rainbow that never
233
+ went away no matter how far `rim` came down is `shimmer`, and the soft light
234
+ still hugging a panel at `elevation={0}` is `glow` - it is cast by the plasma,
235
+ not by the shadow, which really is off at `0`.
92
236
 
93
237
  ## Custom moods
94
238
 
@@ -133,15 +277,16 @@ NOTE: `flow` ripples the outline, so leave it at `0` whenever flush edges should
133
277
 
134
278
  - Use Plasma for container components: panels, docks, cards, dialogs. Components should be nested inside.
135
279
  - 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.
280
+ - Up to `maxSurfaces` (default 16) draw at once; offscreen panels are skipped first, and the library warns in the console when it has to drop any. Two render passes loop over every slot per pixel, so set this value only as high as you need. Changing it recompiles the shaders, so change it when the layout changes, not per frame.
137
281
  - 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.
282
+ - `prefers-reduced-motion` disables Lean, Pulse, the pointer Drop, and Spring - and the form-in, so `plasmaformed` fires at once.
283
+ - A second provider above a scrim (`ground="clear"`) is a full extra render pass; mount it with the dialog and unmount it with the dialog.
139
284
 
140
285
  ## Roadmap
141
286
 
142
287
  Ordered by priority:
143
288
 
144
- 1. **Layers** - panels that will stack instead of fusing. For use with dialogs, menus, and such.
289
+ 1. **Layers** - panels that will stack instead of fusing. A dialog above a scrim is possible today with a second provider ([above a scrim](#above-a-scrim-a-second-canvas)); true stacking inside one canvas is still to come.
145
290
  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.
146
291
  3. **Scroll clipping** - plasma confined to scrollable containers.
147
292
  4. **Pluggable Backgrounds** - colors, images, and live canvas/video shipped in 0.1 (`background` prop); custom shaders are next.
@@ -153,6 +298,14 @@ Contributions welcome for any of these - see [CONTRIBUTING.md](CONTRIBUTING.md).
153
298
 
154
299
  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.
155
300
 
301
+ **Use it on the desktop.** Every pass is full-viewport, so the cost scales with
302
+ the canvas, and phones pay it at a device pixel ratio the effect does not need.
303
+ It runs on mobile - resolution drops past a pixel budget, and `freezeOnScroll`
304
+ pins the last frame through a fling - but it is not where this belongs.
305
+
306
+ Server rendering works: surfaces come out as the CSS fallback with no layout
307
+ effect warnings, and the canvas takes over on hydration.
308
+
156
309
  ## Development
157
310
 
158
311
  ```bash
package/dist/Plasma.d.ts CHANGED
@@ -3,9 +3,8 @@ export interface Offset {
3
3
  x: number;
4
4
  y: number;
5
5
  }
6
- export interface PlasmaProps extends Omit<React.HTMLAttributes<HTMLElement>, "onDragStart" | "onDragEnd"> {
7
- /** Element to render. Default "div". */
8
- as?: React.ElementType;
6
+ /** The props Plasma itself understands. Everything else goes to the rendered element. */
7
+ export interface PlasmaOwnProps {
9
8
  /** Corner radius in px. Defaults to the provider's radius. */
10
9
  radius?: number;
11
10
  /** How far (px) the surface leans toward the pointer while standalone. 0 or false disables. Default 10. */
@@ -26,8 +25,19 @@ export interface PlasmaProps extends Omit<React.HTMLAttributes<HTMLElement>, "on
26
25
  draggable?: boolean;
27
26
  /** Snap on release (edges latch to neighbors, otherwise the grid). Default true. */
28
27
  snap?: boolean;
29
- /** Keep dragging inside this element. Defaults to the viewport. Also sets the grid origin. */
30
- bounds?: React.RefObject<HTMLElement>;
28
+ /**
29
+ * Only snap against surfaces carrying the same group. Two independent sets
30
+ * of panels on one page stop latching onto each other. Surfaces with no
31
+ * group form one group of their own. Scopes snapping only - surfaces still
32
+ * fuse visually wherever they overlap.
33
+ */
34
+ group?: string;
35
+ /**
36
+ * Keep dragging inside this element. Defaults to the viewport. Also sets the
37
+ * grid origin. The `| null` matters: under @types/react 19 `useRef<T>(null)`
38
+ * is a `RefObject<T | null>`, which a bare `RefObject<HTMLElement>` rejects.
39
+ */
40
+ bounds?: React.RefObject<HTMLElement | null>;
31
41
  /** Controlled offset from the element's layout position. Changes spring into place. */
32
42
  offset?: Offset;
33
43
  /** Starting offset when uncontrolled. */
@@ -37,5 +47,24 @@ export interface PlasmaProps extends Omit<React.HTMLAttributes<HTMLElement>, "on
37
47
  onDragEnd?: (offset: Offset) => void;
38
48
  /** Fires when the surface fuses with or separates from a neighbor. */
39
49
  onJoinChange?: (joined: boolean) => void;
50
+ /** Fires as the surface starts forming in (not under reduced motion). The element carries `data-plasma-forming` meanwhile. */
51
+ onForming?: () => void;
52
+ /** Fires once the surface has formed in — at once under reduced motion. Reveal the contents here. */
53
+ onFormed?: () => void;
40
54
  }
41
- export declare const Plasma: React.ForwardRefExoticComponent<PlasmaProps & React.RefAttributes<HTMLElement>>;
55
+ /**
56
+ * Props for `<Plasma as={C}>`: Plasma's own, plus everything `C` accepts.
57
+ * `PlasmaProps` on its own still means the div form, as it always did.
58
+ */
59
+ export type PlasmaProps<C extends React.ElementType = "div"> = PlasmaOwnProps & {
60
+ /** Element or component to render. Default "div". */
61
+ as?: C;
62
+ ref?: React.Ref<HTMLElement>;
63
+ } & Omit<React.ComponentPropsWithoutRef<C>, keyof PlasmaOwnProps | "as" | "ref">;
64
+ /**
65
+ * A plasma surface. The DOM stays ordinary HTML; the canvas only draws.
66
+ *
67
+ * The cast is what makes `as` polymorphic: `<Plasma as="a" href=...>` and
68
+ * `<Plasma as={Link} to=...>` typecheck, which a plain forwardRef cannot express.
69
+ */
70
+ export declare const Plasma: <C extends React.ElementType = "div">(props: PlasmaProps<C>) => React.ReactElement | null;
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { BackgroundSource, PlasmaRenderer } from "./renderer";
2
+ import { BackgroundSource, MaterialName, PlasmaRenderer } from "./renderer";
3
3
  import { Mood, MoodName } from "./moods";
4
4
  export interface PlasmaProviderProps {
5
5
  children?: React.ReactNode;
@@ -23,6 +23,69 @@ export interface PlasmaProviderProps {
23
23
  highlight?: number;
24
24
  /** Strength of the thin line along the outline. 0 turns it off. Default 1. */
25
25
  edgeLine?: number;
26
+ /**
27
+ * The slow iridescent sheen that drifts across the body of each surface.
28
+ * 0 turns it off. Default 1. Set it to 0 along with `rim`, `highlight`,
29
+ * `edgeLine`, `glow` and `wash` for plain water - see "Clear as water" in
30
+ * the README.
31
+ */
32
+ shimmer?: number;
33
+ /** Colored bloom the plasma casts onto the background around it. 0 turns it off. Default 1. */
34
+ glow?: number;
35
+ /**
36
+ * How much of its own cast the material puts on what you see through it -
37
+ * a slight desaturation and lift. 0 passes the background through
38
+ * untouched. Default 1.
39
+ */
40
+ wash?: number;
41
+ /** Film grain over the background (never over the surfaces). 0 turns it off. Default 1. */
42
+ grain?: number;
43
+ /**
44
+ * What the surfaces are made of: "plasma" (the default), "crystal",
45
+ * "metal", "wood", "stone" or "cloud". Every material shares the same
46
+ * geometry, springs and fusing and differs only in how it is shaded.
47
+ */
48
+ material?: MaterialName;
49
+ /**
50
+ * Where the one light comes from, as a direction. Every opaque material
51
+ * reads it, so two of them on a page agree about the sun. Default is up and
52
+ * to the left, in front.
53
+ */
54
+ lightDir?: [number, number, number];
55
+ /** Surface finish for metal: 0 is a mirror, 1 is chalk. Default 0.28. */
56
+ roughness?: number;
57
+ /** How far a highlight stretches along the grain. 0 is isotropic. Default 0. */
58
+ anisotropy?: number;
59
+ /**
60
+ * How far the outline is displaced from its rounded box, in CSS px. A
61
+ * rounded rectangle is right for a liquid and wrong for almost everything
62
+ * else: stone chips, cloud billows, cut metal does neither. Default 0.
63
+ */
64
+ edge?: number;
65
+ /** Size of that displacement, in cycles per px: small is billows, large is chips. Default 0.01. */
66
+ edgeScale?: number;
67
+ /** 0 rolls the displaced edge, 1 breaks it into flats and points. Default 0. */
68
+ edgeSharpness?: number;
69
+ /** How thick a panel is as a solid, in CSS px. The marched materials light a body of this depth. Default 18. */
70
+ thickness?: number;
71
+ /** Surface tension: how hard a material pulls its own shape toward a bead, and how eagerly two merge. Default 0. */
72
+ tension?: number;
73
+ /** Blur the background itself, in CSS px, 0-40. Softens the whole field, unlike `frost`, which blurs only what a frosted surface sees. Default 0. */
74
+ backgroundBlur?: number;
75
+ /**
76
+ * What the canvas shows where there is no surface. "field" paints the
77
+ * background everywhere. "clear" leaves it transparent, so this provider's
78
+ * canvas can sit above other content - a dialog above a scrim - and draw
79
+ * only its surfaces, their shadows and rims; what they refract is the
80
+ * `background` source, usually another provider's canvas. Default "field".
81
+ */
82
+ ground?: "field" | "clear";
83
+ /**
84
+ * Keep each frame after it is shown, so another provider can pass this
85
+ * canvas as its `background` and sample it live (the pair a clear-ground
86
+ * overlay needs). Costs a copy per frame. Fixed at creation. Default false.
87
+ */
88
+ preserveDrawingBuffer?: boolean;
26
89
  /** How thick the material feels: 0 is watery and bouncy, 1 is slow like syrup. Also scales drag and snap springs. Default 0.5. */
27
90
  viscosity?: number;
28
91
  /** How far the plasma trails and stretches behind moving panels. 0 turns it off. Default 1. */
@@ -60,27 +123,73 @@ export interface PlasmaProviderProps {
60
123
  * if any, to scroll with the page.
61
124
  */
62
125
  freezeOnScroll?: boolean;
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. */
126
+ /** Maximum visible plasma surfaces at once. Raising it costs GPU time; changing it rebuilds the shaders. Default 16. */
64
127
  maxSurfaces?: number;
65
- /** z-index of the fixed canvas. Default -1 (behind content). */
128
+ /** z-index of the canvas this provider renders. Default -1 (behind content). Ignored when `canvas` is false. */
66
129
  zIndex?: number;
130
+ /**
131
+ * Whether the provider renders the canvas itself. Set false and place a
132
+ * `<PlasmaCanvas />` anywhere in the tree to control where the element
133
+ * lives and how it is styled. Default true.
134
+ */
135
+ canvas?: boolean;
67
136
  }
68
- export interface PlasmaContextValue {
137
+ /**
138
+ * The parts of the context that exist for the provider's lifetime. This value
139
+ * is stable: it changes once when the renderer is created and then only if
140
+ * the reduced-motion preference does, so `usePlasmaRuntime()` consumers are
141
+ * not re-rendered by every styling change.
142
+ */
143
+ export interface PlasmaRuntime {
69
144
  renderer: PlasmaRenderer | null;
70
- /** Provider-level tint and opacity, used by the CSS fallback. */
145
+ /** False when WebGL2 is unavailable; <Plasma> falls back to a CSS frosted panel. */
146
+ supported: boolean;
147
+ reducedMotion: boolean;
148
+ pulse: (x: number, y: number, strength?: number) => void;
149
+ bump: (energy: number) => void;
150
+ }
151
+ /** The provider-level values a surface falls back to, and the layout settings. */
152
+ export interface PlasmaDefaults {
71
153
  tint: string;
72
154
  opacity: number;
73
155
  frost: number;
74
- /** Provider-level default corner radius. */
75
156
  radius: number;
76
- /** False when WebGL2 is unavailable; <Plasma> falls back to a CSS frosted panel. */
77
- supported: boolean;
78
157
  grid: number;
79
158
  magnet: number;
80
159
  spring: Mood["spring"];
81
- reducedMotion: boolean;
82
- pulse: (x: number, y: number, strength?: number) => void;
83
- bump: (energy: number) => void;
84
160
  }
85
- export declare const usePlasma: () => PlasmaContextValue;
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;
161
+ /** Everything `usePlasma()` returns: the runtime and the defaults together. */
162
+ export interface PlasmaContextValue extends PlasmaRuntime, PlasmaDefaults {
163
+ }
164
+ /** Renderer, support flag and commands. Stable - use this when you only need `pulse`. */
165
+ export declare const usePlasmaRuntime: () => PlasmaRuntime;
166
+ /** Provider-level tint, opacity, frost, radius, grid, magnet and spring. */
167
+ export declare const usePlasmaDefaults: () => PlasmaDefaults;
168
+ /** Everything at once. Re-renders on any provider change; prefer the narrower hooks. */
169
+ export declare function usePlasma(): PlasmaContextValue;
170
+ /** True in every build except a production one. */
171
+ export declare const DEV: boolean;
172
+ /**
173
+ * useLayoutEffect on the client, useEffect on the server - React warns about
174
+ * the former during SSR, and none of this work means anything there anyway.
175
+ */
176
+ export declare const useIsoLayoutEffect: typeof React.useLayoutEffect;
177
+ /** Keeps a ref pointing at the newest value without writing to it during render. */
178
+ export declare function useLatest<T>(value: T): React.MutableRefObject<T>;
179
+ export interface PlasmaCanvasProps {
180
+ className?: string;
181
+ style?: React.CSSProperties;
182
+ /** z-index of the canvas. Default -1 (behind content). */
183
+ zIndex?: number;
184
+ }
185
+ /**
186
+ * The canvas the plasma is drawn on. The provider renders one by default;
187
+ * render this yourself (with `canvas={false}` on the provider) to choose
188
+ * where the element sits in the DOM and how it is styled.
189
+ *
190
+ * The drawn region is still the whole viewport - this controls the element,
191
+ * not the area the renderer covers. Clipping the field to a container is not
192
+ * supported yet.
193
+ */
194
+ export declare function PlasmaCanvas({ className, style, zIndex }: PlasmaCanvasProps): React.JSX.Element;
195
+ 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, shimmer, glow, wash, grain, backgroundBlur, ground, preserveDrawingBuffer, material, lightDir, roughness, anisotropy, edge, edgeScale, edgeSharpness, thickness, tension, pointerDrop, ambientDrops, grid, magnet, quality, maxSurfaces, zIndex, freezeOnScroll, canvas, }: PlasmaProviderProps): React.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,23 @@
1
- export { PlasmaProvider, usePlasma } from "./PlasmaProvider";
2
- export type { PlasmaProviderProps, PlasmaContextValue } from "./PlasmaProvider";
1
+ export { PlasmaProvider, PlasmaCanvas, usePlasma, usePlasmaRuntime, usePlasmaDefaults } from "./PlasmaProvider";
2
+ export type { PlasmaProviderProps, PlasmaCanvasProps, PlasmaContextValue, PlasmaRuntime, PlasmaDefaults } from "./PlasmaProvider";
3
3
  export { Plasma } from "./Plasma";
4
- export type { PlasmaProps, Offset } from "./Plasma";
4
+ export type { PlasmaProps, PlasmaOwnProps, Offset } from "./Plasma";
5
5
  export { moods, resolveMood } from "./moods";
6
6
  export type { Mood, MoodName } from "./moods";
7
7
  export { snapBox, boxGap } from "./snap";
8
8
  export type { Box, SnapOptions } from "./snap";
9
+ /**
10
+ * The rendering engine. Exported as an escape hatch - `usePlasmaRuntime()`
11
+ * hands you the live instance - but it is internal: its shape tracks whatever
12
+ * the shaders need and can change in any release, including a patch. Nothing
13
+ * below this line is covered by the package's semver promise.
14
+ * @internal
15
+ */
9
16
  export { PlasmaRenderer } from "./renderer";
17
+ /** @internal */
10
18
  export { makeShaders, DEFAULT_MAX_SHAPES } from "./shaders";
19
+ /** @internal */
11
20
  export type { BackgroundSource, JoinedSides, RendererSettings, ShapeHandle, ShapeOptions } from "./renderer";
21
+ /** The materials a surface can be made of. */
22
+ export { MATERIALS, FORMING_ATTR, FORMING_EVENT, FORMED_EVENT } from "./renderer";
23
+ export type { MaterialName } from "./renderer";