@cruxgarden/plasma-ui 0.2.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +162 -9
- package/dist/Plasma.d.ts +35 -6
- package/dist/PlasmaProvider.d.ts +122 -13
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1011 -165
- package/dist/renderer.d.ts +85 -2
- package/package.json +7 -3
package/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.
|
|
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
|
-
| `
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
/**
|
|
30
|
-
|
|
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
|
-
|
|
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;
|
package/dist/PlasmaProvider.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
86
|
-
export
|
|
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";
|