@cruxgarden/plasma-ui 0.1.2 → 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 +41 -1
- package/README.md +30 -54
- package/dist/PlasmaProvider.d.ts +14 -7
- package/dist/index.js +219 -55
- package/dist/renderer.d.ts +52 -1
- package/dist/shaders.d.ts +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
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
|
+
|
|
29
|
+
## 0.2.0
|
|
30
|
+
|
|
31
|
+
**Breaking:** the CSS class on every surface is `.plasma-panel`, was `.plasma-glass`.
|
|
32
|
+
Only matters if you targeted it in your own stylesheet.
|
|
33
|
+
|
|
34
|
+
- The material is called plasma throughout: prop descriptions, docs, and the
|
|
35
|
+
shader's own names. "Glass" is gone except where the readme credits Apple's
|
|
36
|
+
Liquid Glass as prior art.
|
|
37
|
+
- `homepage` points at the site rather than the readme.
|
|
38
|
+
|
|
39
|
+
## 0.1.2
|
|
40
|
+
|
|
41
|
+
- Packaging and metadata only; no runtime change.
|
|
42
|
+
|
|
3
43
|
## 0.1.0
|
|
4
44
|
|
|
5
45
|
Initial release.
|
|
6
46
|
|
|
7
47
|
- `PlasmaProvider`: shared WebGL material with moods, theme, tint/opacity/frost, rim (color, width, highlight, edge line), viscosity/stretch/flow, blend, smoothness, refraction, dispersion, grid/magnet, quality.
|
|
8
|
-
- `Plasma`: any element as a
|
|
48
|
+
- `Plasma`: any element as a plasma surface, with per-surface radius, lean, tint/opacity/frost/elevation, join-aware `padding`, drag with edge/grid snapping, controlled offsets, join events.
|
|
9
49
|
- `usePlasma`: pulse, bump, capability and spring info.
|
|
10
50
|
- CSS frosted fallback when WebGL2 is unavailable; `prefers-reduced-motion` support.
|
|
11
51
|
- `fuse={false}`: surfaces that never blend, bridge, or join - for bars, docks, and fixed chrome.
|
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Plasma UI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Liquid panels for React, rendered in WebGL on canvas, inspired by Apple's Liquid Glass design. The `<Plasma>` panel looks and behaves like liquid, with surface tension that fuses on contact with other panels. Anything visible behind the panel is refracted. And for layout convenience, the panels ultimately snap to a grid layout. The library is a work in progress, extracted from the [Crux Garden](https://github.com/cruxgarden) project, but it seemed useful enough to share in its current form.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
|
-
[
|
|
7
|
+
[Playground and Docs](https://cruxgarden.github.io/plasma-ui/) · [Workspace example](https://cruxgarden.github.io/plasma-ui/examples/workspace/)
|
|
8
8
|
|
|
9
|
-
**Status: 0.
|
|
9
|
+
**Status: 0.2.0.** Core is stable and tested, but the API may change.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install @cruxgarden/plasma-ui
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Zero dependencies
|
|
15
|
+
Zero dependencies, except for React.
|
|
16
16
|
|
|
17
17
|
```tsx
|
|
18
18
|
import { PlasmaProvider, Plasma } from "@cruxgarden/plasma-ui";
|
|
@@ -33,31 +33,7 @@ export function App() {
|
|
|
33
33
|
|
|
34
34
|
## Example
|
|
35
35
|
|
|
36
|
-
[`examples/workspace`](examples/workspace)
|
|
37
|
-
|
|
38
|
-
## Not yet
|
|
39
|
-
|
|
40
|
-
Plasma UI 0.1 is a workspace library, not a full UI system. Know these before adopting:
|
|
41
|
-
|
|
42
|
-
- **No layers.** Overlapping surfaces fuse. Dialogs, menus, toasts, and fixed bars must be plain CSS for now (the docs site's own nav shows the pattern).
|
|
43
|
-
- **No clipping in scroll containers.** Plasma inside a scrollable list draws past its edges. Scrolling _inside_ one panel is fine.
|
|
44
|
-
- **No drag or resize handles.** `draggable` moves the whole surface; mark interactive children `data-plasma-nodrag`.
|
|
45
|
-
- **Draws its own background.** Browsers don't let WebGL read the rendered page, so the glass 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 frosted glass.
|
|
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
|
|
|
@@ -69,7 +45,7 @@ Without WebGL2, `Plasma` falls back to a CSS frosted panel.
|
|
|
69
45
|
| `background` | `BackgroundSource` | | Any CSS color (luminance drift), image URL (refracted, slow swirl), or an `img`/`canvas`/`video` element - canvas and video update live. Dynamic. Omit for the procedural mood field |
|
|
70
46
|
| `blend` | `number` | mood | Distance (px) at which surfaces start to fuse |
|
|
71
47
|
| `viscosity` | `number` | `0.5` | `0` is watery and bouncy, `1` is slow like syrup; also scales drag and snap springs |
|
|
72
|
-
| `stretch` | `number` | `1` | How far the
|
|
48
|
+
| `stretch` | `number` | `1` | How far the plasma trails behind moving panels; `0` turns it off |
|
|
73
49
|
| `flow` | `number` | `0` | Slow ripple along the edges |
|
|
74
50
|
| `tint` | `string` | `"#ffffff"` | Plasma color (hex) |
|
|
75
51
|
| `opacity` | `number` | `0` | Tint strength, 0 (clear) to 1 (solid color) |
|
|
@@ -91,7 +67,7 @@ Without WebGL2, `Plasma` falls back to a CSS frosted panel.
|
|
|
91
67
|
|
|
92
68
|
## `<Plasma>`
|
|
93
69
|
|
|
94
|
-
Accepts all HTML attributes plus:
|
|
70
|
+
Accepts all HTML attributes, plus the following:
|
|
95
71
|
|
|
96
72
|
| Prop | Type | Default | Description |
|
|
97
73
|
| --------------------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -126,19 +102,19 @@ const dusk: Mood = {
|
|
|
126
102
|
};
|
|
127
103
|
```
|
|
128
104
|
|
|
129
|
-
## Motion
|
|
105
|
+
## Motion
|
|
130
106
|
|
|
131
|
-
Each
|
|
107
|
+
Each panel undulates like a Slinky when moving. Three properties control this movement: viscosity, stretch, and flow.
|
|
132
108
|
|
|
133
109
|
```tsx
|
|
134
110
|
<PlasmaProvider viscosity={0.1} stretch={1.3} flow={0.6} /> // water
|
|
135
|
-
<PlasmaProvider viscosity={0.85} stretch={1.8} />
|
|
136
|
-
<PlasmaProvider stretch={0} />
|
|
111
|
+
<PlasmaProvider viscosity={0.85} stretch={1.8} /> // honey
|
|
112
|
+
<PlasmaProvider stretch={0} /> // the plasma tracks panels exactly
|
|
137
113
|
```
|
|
138
114
|
|
|
139
|
-
`flow` ripples the outline, so leave it at `0`
|
|
115
|
+
NOTE: `flow` ripples the outline, so leave it at `0` whenever flush edges should stay perfectly straight.
|
|
140
116
|
|
|
141
|
-
## Styling the rim
|
|
117
|
+
## Styling the rim (fancy outline)
|
|
142
118
|
|
|
143
119
|
```tsx
|
|
144
120
|
// solid cyan rim, a bit wider, no pointer highlight
|
|
@@ -149,33 +125,33 @@ Each surface is a spring chasing its element, and the drawn glass always covers
|
|
|
149
125
|
<Plasma tint="#ff5fa2" opacity={0.3} />
|
|
150
126
|
</PlasmaProvider>
|
|
151
127
|
|
|
152
|
-
// plain
|
|
128
|
+
// plain plasma: no colored rim, just the edge line
|
|
153
129
|
<PlasmaProvider rim={0} />
|
|
154
130
|
```
|
|
155
131
|
|
|
156
132
|
## Guidelines
|
|
157
133
|
|
|
158
|
-
- Use
|
|
159
|
-
- Place surfaces
|
|
160
|
-
- Up to `maxSurfaces` (default 16) draw at once; offscreen
|
|
161
|
-
- Lean and
|
|
162
|
-
- `prefers-reduced-motion` disables
|
|
134
|
+
- Use Plasma for container components: panels, docks, cards, dialogs. Components should be nested inside.
|
|
135
|
+
- Place surfaces together or further apart than the Blend distance. Smaller gaps render as liquid bridging them.
|
|
136
|
+
- Up to `maxSurfaces` (default 16) draw at once; offscreen panels are skipped first. Two render passes loop over every slot per pixel, so set this value only as high as you need.
|
|
137
|
+
- Lean and Pulse use the CSS `translate` and `scale` properties, and Drag uses `transform`. Avoid setting these properties on `Plasma` elements yourself.
|
|
138
|
+
- `prefers-reduced-motion` disables Lean, Pulse, the pointer Drop, and Spring.
|
|
163
139
|
|
|
164
140
|
## Roadmap
|
|
165
141
|
|
|
166
|
-
|
|
142
|
+
Ordered by priority:
|
|
167
143
|
|
|
168
|
-
1. **Layers** -
|
|
169
|
-
2. **Drag handles and resize** - `handle` prop so panel content
|
|
170
|
-
3. **Scroll clipping** -
|
|
171
|
-
4. **Pluggable
|
|
172
|
-
5. **Shapes** -
|
|
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.
|
|
146
|
+
3. **Scroll clipping** - plasma confined to scrollable containers.
|
|
147
|
+
4. **Pluggable Backgrounds** - colors, images, and live canvas/video shipped in 0.1 (`background` prop); custom shaders are next.
|
|
148
|
+
5. **Shapes and Orientation** - non-rectangular outlines, rotation...etc.
|
|
173
149
|
|
|
174
|
-
Contributions welcome
|
|
150
|
+
Contributions welcome for any of these - see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
175
151
|
|
|
176
152
|
## Browser support
|
|
177
153
|
|
|
178
|
-
Chrome, Edge, Firefox, and Safari 16.4+ (WebGL2).
|
|
154
|
+
Chrome, Edge, Firefox, and Safari 16.4+ (WebGL2). In non-supported browsers, `Plasma` renders as a CSS frosted panel and all layout, drag, and snap behavior still works.
|
|
179
155
|
|
|
180
156
|
## Development
|
|
181
157
|
|
|
@@ -202,7 +178,7 @@ does — see [PUBLISH.md](PUBLISH.md).
|
|
|
202
178
|
|
|
203
179
|
## Used by
|
|
204
180
|
|
|
205
|
-
- [Crux Garden](https://github.com/cruxgarden) - the
|
|
181
|
+
- [Crux Garden](https://github.com/cruxgarden) - the project Plasma UI was originally built for.
|
|
206
182
|
|
|
207
183
|
Using it in something? Add yours in a PR.
|
|
208
184
|
|
|
@@ -225,7 +201,7 @@ Plasma UI is built on well-known graphics and simulation techniques:
|
|
|
225
201
|
- **Blobby surfaces / metaballs** - the fuse-on-contact behavior descends from Jim Blinn's [_A Generalization of Algebraic Surface Drawing_](https://dl.acm.org/doi/10.1145/357306.357310) (1982).
|
|
226
202
|
- **Signed distance fields** - the material is drawn with 2D SDFs combined by smooth minimum, per Inigo Quilez's [2D distance functions](https://iquilezles.org/articles/distfunctions2d/) and [smooth minimum](https://iquilezles.org/articles/smin/) articles; the procedural background uses his [fBM](https://iquilezles.org/articles/fbm/) construction.
|
|
227
203
|
- **Spring integration** - panel motion uses semi-implicit Euler with fixed substeps, in the spirit of Glenn Fiedler's [_Integration Basics_](https://gafferongames.com/post/integration_basics/).
|
|
228
|
-
- **
|
|
204
|
+
- **The optical treatment** (refraction, dispersion, frost) is an original WebGL take on the direction popularized by Apple's [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials) material.
|
|
229
205
|
|
|
230
206
|
## License
|
|
231
207
|
|
package/dist/PlasmaProvider.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface PlasmaProviderProps {
|
|
|
25
25
|
edgeLine?: number;
|
|
26
26
|
/** 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
27
|
viscosity?: number;
|
|
28
|
-
/** How far the
|
|
28
|
+
/** How far the plasma trails and stretches behind moving panels. 0 turns it off. Default 1. */
|
|
29
29
|
stretch?: number;
|
|
30
30
|
/** Slow ripple along the edges. Default 0 (still edges). */
|
|
31
31
|
flow?: number;
|
|
@@ -35,9 +35,9 @@ export interface PlasmaProviderProps {
|
|
|
35
35
|
radius?: number;
|
|
36
36
|
/** Plasma tint color (hex). Default "#ffffff". */
|
|
37
37
|
tint?: string;
|
|
38
|
-
/** Tint strength from 0 (clear
|
|
38
|
+
/** Tint strength from 0 (clear) to 1 (solid color). Default 0. */
|
|
39
39
|
opacity?: number;
|
|
40
|
-
/** Translucency from 0 (clear
|
|
40
|
+
/** Translucency from 0 (clear) to 1 (frosted). Default 0. */
|
|
41
41
|
frost?: number;
|
|
42
42
|
/** How high surfaces float: 0 sits flat with no shadow, 1 floats high with a deep soft shadow. Dragged surfaces raise automatically. Default 0.35. */
|
|
43
43
|
elevation?: number;
|
|
@@ -47,13 +47,20 @@ export interface PlasmaProviderProps {
|
|
|
47
47
|
pointerDrop?: boolean;
|
|
48
48
|
/** Decorative drops orbiting near the bottom right. Default false. */
|
|
49
49
|
ambientDrops?: boolean;
|
|
50
|
-
/** Grid cell size used when draggable
|
|
50
|
+
/** Grid cell size used when a draggable panel snaps. Default 24. */
|
|
51
51
|
grid?: number;
|
|
52
52
|
/** Edge latch distance for snapping. Default 40. */
|
|
53
53
|
magnet?: number;
|
|
54
54
|
/** Maximum device pixel ratio for the canvas. Default 1.25. */
|
|
55
55
|
quality?: number;
|
|
56
|
-
/**
|
|
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;
|
|
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). */
|
|
59
66
|
zIndex?: number;
|
|
@@ -66,7 +73,7 @@ export interface PlasmaContextValue {
|
|
|
66
73
|
frost: number;
|
|
67
74
|
/** Provider-level default corner radius. */
|
|
68
75
|
radius: number;
|
|
69
|
-
/** False when WebGL2 is unavailable; <Plasma> falls back to CSS
|
|
76
|
+
/** False when WebGL2 is unavailable; <Plasma> falls back to a CSS frosted panel. */
|
|
70
77
|
supported: boolean;
|
|
71
78
|
grid: number;
|
|
72
79
|
magnet: number;
|
|
@@ -76,4 +83,4 @@ export interface PlasmaContextValue {
|
|
|
76
83
|
bump: (energy: number) => void;
|
|
77
84
|
}
|
|
78
85
|
export declare const usePlasma: () => PlasmaContextValue;
|
|
79
|
-
export declare function PlasmaProvider({ children, mood, theme, blend, refraction, dispersion, rim, smoothness, background, radius, tint, opacity, frost, elevation, viscosity, stretch, flow, rimColor, rimWidth, highlight, edgeLine, pointerDrop, ambientDrops, grid, magnet, quality, maxSurfaces, zIndex, }: PlasmaProviderProps): React.JSX.Element;
|
|
86
|
+
export declare function PlasmaProvider({ children, mood, theme, blend, refraction, dispersion, rim, smoothness, background, radius, tint, opacity, frost, elevation, viscosity, stretch, flow, rimColor, rimWidth, highlight, edgeLine, pointerDrop, ambientDrops, grid, magnet, quality, maxSurfaces, zIndex, freezeOnScroll, }: PlasmaProviderProps): React.JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ in vec2 p; void main(){ gl_Position = vec4(p, 0., 1.); }`;
|
|
|
9
9
|
function makeShaders(MAX_SHAPES) {
|
|
10
10
|
const common = `
|
|
11
11
|
precision highp float;
|
|
12
|
-
uniform vec2 uRes; uniform float uScale, uTime, uGoo, uEnergy, uLight, uMouseAmt, uDropR, uAmbient, uScroll, uVisc, uFlow;
|
|
12
|
+
uniform vec2 uRes; uniform vec4 uView; uniform float uScale, uTime, uGoo, uEnergy, uLight, uMouseAmt, uDropR, uAmbient, uScroll, uVisc, uFlow;
|
|
13
13
|
uniform vec2 uMouse;
|
|
14
14
|
uniform vec4 uP[${MAX_SHAPES}]; uniform vec4 uR[${MAX_SHAPES}]; uniform float uF[${MAX_SHAPES}]; uniform vec4 uT[${MAX_SHAPES}]; uniform float uFr[${MAX_SHAPES}]; uniform float uEl[${MAX_SHAPES}]; uniform float uSolo[${MAX_SHAPES}];
|
|
15
15
|
uniform int uCount;
|
|
@@ -61,7 +61,7 @@ float scene(vec2 p){
|
|
|
61
61
|
d = min(d, ds);
|
|
62
62
|
if(uAmbient > .5){
|
|
63
63
|
float t = uTime*.4;
|
|
64
|
-
vec2 home = vec2(
|
|
64
|
+
vec2 home = uView.xy + vec2(uView.z*.88, uView.w*.74);
|
|
65
65
|
d = smin(d, length(p - home - vec2(cos(t),sin(t*1.3))*70.) - 38., uGoo);
|
|
66
66
|
d = smin(d, length(p - home - vec2(cos(t*1.7+2.),sin(t*.9+1.))*90.) - 26., uGoo);
|
|
67
67
|
d = smin(d, length(p - home - vec2(sin(t*.7),cos(t*1.1))*40.) - 20., uGoo);
|
|
@@ -139,7 +139,7 @@ vec3 bg(vec2 p){
|
|
|
139
139
|
col = mix(col, uC, smoothstep(.55,.9, w.x*n*1.6));
|
|
140
140
|
float lines = abs(fract(n*14.)-.5);
|
|
141
141
|
col += (1.-smoothstep(0.,.06,lines)) * .07 * (0.6+uEnergy);
|
|
142
|
-
vec2 v = (p - vec2(0., uScroll))/
|
|
142
|
+
vec2 v = (p - vec2(0., uScroll) - uView.xy)/uView.zw - .5;
|
|
143
143
|
col *= .55 + .45*smoothstep(1.2,.2,length(v));
|
|
144
144
|
return mix(col, mix(vec3(.90,.93,.95), col, .42), uLight);
|
|
145
145
|
}
|
|
@@ -168,8 +168,8 @@ void main(){
|
|
|
168
168
|
+ vec2(fbm(q*.004 + uTime*.05) - .5, fbm(q*.004 + 31.7 - uTime*.04) - .5) * 22.)
|
|
169
169
|
* (.5 + uFlow*.5);
|
|
170
170
|
// cover-fit the image to the canvas
|
|
171
|
-
float sc = max(
|
|
172
|
-
vec2 uv = (q - .5*
|
|
171
|
+
float sc = max(uView.z / uImgRes.x, uView.w / uImgRes.y);
|
|
172
|
+
vec2 uv = (q - uView.xy - .5*uView.zw) / (uImgRes * sc) + .5;
|
|
173
173
|
o = vec4(texture(uImg, clamp(uv, 0., 1.)).rgb, 1.);
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
@@ -185,7 +185,7 @@ out vec4 o;
|
|
|
185
185
|
vec3 pal(float t){ return .5 + .5*cos(6.2831*(t + vec3(0., .33, .67))); }
|
|
186
186
|
float H(vec2 uv){ return texture(uH, uv).r; }
|
|
187
187
|
vec2 uvAt(vec2 q){ return vec2(q.x, uRes.y - q.y) / uRes; }
|
|
188
|
-
// background seen through
|
|
188
|
+
// background seen through plasma with frost f: sharp, then medium, then heavy blur
|
|
189
189
|
vec3 seen(vec2 q, float f){
|
|
190
190
|
vec2 u = uvAt(q);
|
|
191
191
|
vec3 sharp = texture(uBg, u).rgb;
|
|
@@ -232,7 +232,10 @@ void main(){
|
|
|
232
232
|
float shStr = .5 * smoothstep(0., .12, el) * clamp(el + .35, 0., 1.);
|
|
233
233
|
vec3 col = back * (1. - shStr*smoothstep(.02, .55, hs)*(1.-uLight*.6));
|
|
234
234
|
float hHere = H(uv);
|
|
235
|
-
|
|
235
|
+
// Palette phases run on viewport position, like the grain: a frame drawn for
|
|
236
|
+
// a taller region must colour its viewport band exactly as the live frame does.
|
|
237
|
+
vec2 vp = p - uView.xy;
|
|
238
|
+
col += pal(uTime*.03 + vp.x/1400.) * .06 * smoothstep(.0, .45, hHere) * (1.+uEnergy);
|
|
236
239
|
|
|
237
240
|
if(sd > -2.){
|
|
238
241
|
vec2 th = 2. / vec2(textureSize(uH, 0));
|
|
@@ -253,7 +256,7 @@ void main(){
|
|
|
253
256
|
seen(p + off*(1.-disp), fr).b
|
|
254
257
|
);
|
|
255
258
|
refr = mix(refr, vec3(dot(refr, vec3(.333))), .18) * mix(1.08, .9, uLight) + .03*(1.-uLight);
|
|
256
|
-
// frosted
|
|
259
|
+
// frosted: milkier and a little brighter
|
|
257
260
|
refr = mix(refr, mix(refr, vec3(dot(refr, vec3(.333))), .25) * mix(1.12, .97, uLight) + mix(.05, .03, uLight), fr);
|
|
258
261
|
float hl = 1. - .55*uLight;
|
|
259
262
|
|
|
@@ -273,19 +276,21 @@ void main(){
|
|
|
273
276
|
if (uRimMode > .5 && uRimMode < 1.5) rimCol = uRimColor * facing * 1.4;
|
|
274
277
|
else if (uRimMode > 1.5) rimCol = tcol * facing * 1.4;
|
|
275
278
|
|
|
276
|
-
vec3
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
vec3 plasma = refr;
|
|
280
|
+
plasma += rimCol * fres * .45 * hl * uRim;
|
|
281
|
+
plasma += vec3(1.) * spec * .75 * hl * uSpec;
|
|
279
282
|
// faint shimmer across the body; fades out as the tint becomes opaque
|
|
280
|
-
|
|
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
|
|
|
284
287
|
float a = smoothstep(-.8, .8, sd);
|
|
285
|
-
col = mix(col,
|
|
288
|
+
col = mix(col, plasma, a);
|
|
286
289
|
grain = 1. - a; // grain is background-only; panels stay clean
|
|
287
290
|
}
|
|
288
|
-
|
|
291
|
+
// Keyed on viewport position, so a frame drawn for a taller region has the
|
|
292
|
+
// same grain in its viewport band as the live frame that replaces it.
|
|
293
|
+
col += (hash(p - uView.xy + uTime) - .5) * .025 * grain;
|
|
289
294
|
o = vec4(col, 1.);
|
|
290
295
|
}`;
|
|
291
296
|
return { maskFrag, tintFrag, blurFrag, bgFrag, compFrag };
|
|
@@ -309,6 +314,7 @@ function hexToRgb(hex) {
|
|
|
309
314
|
// src/renderer.ts
|
|
310
315
|
var UNIFORMS = [
|
|
311
316
|
"uRes",
|
|
317
|
+
"uView",
|
|
312
318
|
"uScale",
|
|
313
319
|
"uTime",
|
|
314
320
|
"uGoo",
|
|
@@ -358,6 +364,7 @@ var UNIFORMS = [
|
|
|
358
364
|
"uHair"
|
|
359
365
|
];
|
|
360
366
|
var MASK_SCALE = 0.5;
|
|
367
|
+
var MAX_PIXELS = 26e5;
|
|
361
368
|
function elementBox(el) {
|
|
362
369
|
const r = el.getBoundingClientRect();
|
|
363
370
|
const w = el.offsetWidth, h = el.offsetHeight;
|
|
@@ -405,7 +412,7 @@ function parseCssColor(src) {
|
|
|
405
412
|
if (r) return [+r[1] / 255, +r[2] / 255, +r[3] / 255];
|
|
406
413
|
return null;
|
|
407
414
|
}
|
|
408
|
-
var
|
|
415
|
+
var _PlasmaRenderer = class _PlasmaRenderer {
|
|
409
416
|
constructor(canvas, gl, settings) {
|
|
410
417
|
this.imgTex = null;
|
|
411
418
|
this.imgRes = [1, 1];
|
|
@@ -418,6 +425,21 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
418
425
|
this.last = 0;
|
|
419
426
|
this.time = 0;
|
|
420
427
|
this.dpr = 1;
|
|
428
|
+
this.resizeQueued = false;
|
|
429
|
+
/** Coarse pointer = touch. Only there does a fling run on the compositor with rAF deferred. */
|
|
430
|
+
this.coarse = typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
|
|
431
|
+
this.frozen = false;
|
|
432
|
+
this.resizeWhileFrozen = false;
|
|
433
|
+
/** Scroll offset the last frame was drawn for: where the pinned image belongs on the page. */
|
|
434
|
+
this.drawnScroll = [0, 0];
|
|
435
|
+
/**
|
|
436
|
+
* The region a frame covers, in CSS px: origin is where the viewport's
|
|
437
|
+
* top-left sits inside it, size is the whole region. Normally the viewport
|
|
438
|
+
* exactly. When pinning for a fling it is three viewports tall, so the
|
|
439
|
+
* scroll has a viewport of runway above and below before it runs out.
|
|
440
|
+
*/
|
|
441
|
+
this.region = { ox: 0, oy: 0, w: 0, h: 0 };
|
|
442
|
+
// viewports of runway above and below when pinned
|
|
421
443
|
this.energy = 0;
|
|
422
444
|
this.mouse = { x: 0, y: 0, tx: 0, ty: 0, amt: 0, target: 0 };
|
|
423
445
|
this.pulses = Array.from({ length: MAX_PULSES }, () => [0, 0, -99, 0]);
|
|
@@ -428,6 +450,53 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
428
450
|
this.lightQuery = typeof matchMedia !== "undefined" ? matchMedia("(prefers-color-scheme: light)") : null;
|
|
429
451
|
this.tintCache = /* @__PURE__ */ new Map();
|
|
430
452
|
this.RP = new Float32Array(MAX_PULSES * 4);
|
|
453
|
+
/**
|
|
454
|
+
* A hidden tab draws nothing anyone can see. Browsers throttle rAF there
|
|
455
|
+
* but do not all stop it, so the loop stops itself and picks up where it
|
|
456
|
+
* left off - `last` is reset so the first frame back does not see a
|
|
457
|
+
* minute-long dt.
|
|
458
|
+
*/
|
|
459
|
+
this.onVisibility = () => {
|
|
460
|
+
if (document.hidden) {
|
|
461
|
+
cancelAnimationFrame(this.raf);
|
|
462
|
+
this.raf = 0;
|
|
463
|
+
} else if (!this.raf && !this.frozen) {
|
|
464
|
+
this.last = 0;
|
|
465
|
+
this.raf = requestAnimationFrame(this.frame);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* A fling on a touch device is driven by the compositor, and rAF is deferred
|
|
470
|
+
* while it runs, so anything JS repositions arrives late: the plasma trails
|
|
471
|
+
* the panels it belongs to. Instead of chasing, the canvas is pinned to the
|
|
472
|
+
* page for the duration - the last drawn frame becomes a texture that
|
|
473
|
+
* scrolls with the content, moved by the compositor with no JS in the path -
|
|
474
|
+
* and the loop resumes once the scroll events stop.
|
|
475
|
+
*
|
|
476
|
+
* The field's scroll parallax is 1:1 on these devices (see draw), so the
|
|
477
|
+
* pinned image and the first live frame after it line up.
|
|
478
|
+
*/
|
|
479
|
+
this.onScroll = () => {
|
|
480
|
+
if (!this.settings.freezeOnScroll) return;
|
|
481
|
+
if (!this.frozen) this.freeze();
|
|
482
|
+
clearTimeout(this.scrollIdle);
|
|
483
|
+
this.scrollIdle = setTimeout(this.thaw, 120);
|
|
484
|
+
};
|
|
485
|
+
this.thaw = () => {
|
|
486
|
+
if (!this.frozen) return;
|
|
487
|
+
const c = this.canvas;
|
|
488
|
+
c.style.top = "";
|
|
489
|
+
c.style.left = "";
|
|
490
|
+
c.style.width = "100%";
|
|
491
|
+
c.style.height = "100%";
|
|
492
|
+
c.style.position = "fixed";
|
|
493
|
+
c.style.inset = "0";
|
|
494
|
+
this.frozen = false;
|
|
495
|
+
this.resizeWhileFrozen = false;
|
|
496
|
+
this.applyResize();
|
|
497
|
+
this.last = 0;
|
|
498
|
+
if (!document.hidden && !this.raf) this.raf = requestAnimationFrame(this.frame);
|
|
499
|
+
};
|
|
431
500
|
this.onPointer = (e) => {
|
|
432
501
|
this.mouse.tx = e.clientX;
|
|
433
502
|
this.mouse.ty = e.clientY;
|
|
@@ -436,24 +505,53 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
436
505
|
this.onLeave = () => {
|
|
437
506
|
this.mouse.target = 0;
|
|
438
507
|
};
|
|
508
|
+
/**
|
|
509
|
+
* Resizing reallocates eight render targets, so it must not run per event.
|
|
510
|
+
* Mobile fires `resize` continuously while the URL bar collapses, which
|
|
511
|
+
* otherwise reallocated every texture repeatedly mid-scroll.
|
|
512
|
+
*/
|
|
439
513
|
this.resize = () => {
|
|
514
|
+
if (this.resizeQueued) return;
|
|
515
|
+
this.resizeQueued = true;
|
|
516
|
+
requestAnimationFrame(() => {
|
|
517
|
+
this.resizeQueued = false;
|
|
518
|
+
this.applyResize();
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
this.applyResize = () => {
|
|
522
|
+
if (this.frozen) {
|
|
523
|
+
this.resizeWhileFrozen = true;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
this.region = { ox: 0, oy: 0, w: innerWidth, h: innerHeight };
|
|
527
|
+
this.allocate();
|
|
528
|
+
};
|
|
529
|
+
/** Size the canvas and every target for the current region. */
|
|
530
|
+
this.allocate = () => {
|
|
440
531
|
const gl = this.gl;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
532
|
+
const { w: rw, h: rh } = this.region;
|
|
533
|
+
let dpr = Math.min(devicePixelRatio || 1, this.settings.quality);
|
|
534
|
+
const pixels = rw * rh * dpr * dpr;
|
|
535
|
+
if (pixels > MAX_PIXELS) dpr *= Math.sqrt(MAX_PIXELS / pixels);
|
|
536
|
+
this.dpr = dpr;
|
|
537
|
+
const cw = Math.max(1, Math.round(rw * this.dpr));
|
|
538
|
+
const ch = Math.max(1, Math.round(rh * this.dpr));
|
|
539
|
+
if (cw === this.canvas.width && ch === this.canvas.height) return;
|
|
540
|
+
this.canvas.width = cw;
|
|
541
|
+
this.canvas.height = ch;
|
|
444
542
|
const w = Math.max(1, Math.round(this.canvas.width * MASK_SCALE));
|
|
445
543
|
const h = Math.max(1, Math.round(this.canvas.height * MASK_SCALE));
|
|
446
|
-
const size = (t, tw, th) => {
|
|
544
|
+
const size = (t, tw, th, color = false) => {
|
|
447
545
|
t.w = tw;
|
|
448
546
|
t.h = th;
|
|
449
547
|
gl.bindTexture(gl.TEXTURE_2D, t.tex);
|
|
450
|
-
if (this.floatOK) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, tw, th, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
548
|
+
if (this.floatOK && !color) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, tw, th, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
451
549
|
else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tw, th, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
452
550
|
gl.bindFramebuffer(gl.FRAMEBUFFER, t.fb);
|
|
453
551
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.tex, 0);
|
|
454
552
|
};
|
|
455
553
|
[this.rtA, this.rtB, this.rtC, this.rtT, this.rtFr, this.rtBgM, this.rtBgH].forEach((t) => size(t, w, h));
|
|
456
|
-
size(this.rtBg, this.canvas.width, this.canvas.height);
|
|
554
|
+
size(this.rtBg, this.canvas.width, this.canvas.height, true);
|
|
457
555
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.mrtFb);
|
|
458
556
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.rtT.tex, 0);
|
|
459
557
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.rtFr.tex, 0);
|
|
@@ -461,7 +559,7 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
461
559
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
462
560
|
};
|
|
463
561
|
this.frame = (now2) => {
|
|
464
|
-
this.raf = requestAnimationFrame(this.frame);
|
|
562
|
+
if (!this.oneShot) this.raf = requestAnimationFrame(this.frame);
|
|
465
563
|
const dt = this.last ? Math.min((now2 - this.last) / 1e3, 0.05) : 0.016;
|
|
466
564
|
this.last = now2;
|
|
467
565
|
const s = this.settings;
|
|
@@ -476,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
|
|
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 >
|
|
588
|
+
if (b.w === 0 || b.l > right || b.t > bottom || b.l + b.w < left || b.t + b.h < top) {
|
|
490
589
|
r.sp.live = false;
|
|
491
590
|
return;
|
|
492
591
|
}
|
|
@@ -532,10 +631,21 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
532
631
|
const le = 1 - Math.exp(-dt * 3);
|
|
533
632
|
r.lx += (tx - r.lx) * le;
|
|
534
633
|
r.ly += (ty - r.ly) * le;
|
|
535
|
-
|
|
634
|
+
if (Math.abs(tx - r.lx) < 0.05 && Math.abs(ty - r.ly) < 0.05) {
|
|
635
|
+
r.lx = tx;
|
|
636
|
+
r.ly = ty;
|
|
637
|
+
}
|
|
638
|
+
const lean = tx === 0 && ty === 0 && r.lx === 0 && r.ly === 0 ? "" : `${r.lx.toFixed(2)}px ${r.ly.toFixed(2)}px`;
|
|
639
|
+
if (lean !== r.leanCss) {
|
|
640
|
+
r.leanCss = lean;
|
|
641
|
+
r.el.style.translate = lean;
|
|
642
|
+
}
|
|
536
643
|
const u = (this.time - r.pulseAt) / 0.36;
|
|
537
|
-
|
|
538
|
-
|
|
644
|
+
const scale = r.pulseAt >= 0 && u >= 0 && u <= 1 && !s.reducedMotion ? String(1 + 0.04 * r.pulseS * Math.sin(Math.PI * u)) : "";
|
|
645
|
+
if (scale !== r.scaleCss) {
|
|
646
|
+
r.scaleCss = scale;
|
|
647
|
+
r.el.style.scale = scale;
|
|
648
|
+
}
|
|
539
649
|
});
|
|
540
650
|
const v = Math.min(Math.max(s.viscosity, 0), 1);
|
|
541
651
|
const st = Math.max(s.stretch, 0);
|
|
@@ -543,7 +653,8 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
543
653
|
const zeta = 0.28 + 0.87 * v;
|
|
544
654
|
const damp = 2 * zeta * Math.sqrt(stiff);
|
|
545
655
|
const sx = scrollX, sy = scrollY;
|
|
546
|
-
const
|
|
656
|
+
const hMax = 0.2 / Math.sqrt(Math.max(stiff, 1e-6));
|
|
657
|
+
const steps = Math.min(240, Math.max(Math.ceil(dt * 120), Math.ceil(dt / hMax), 1));
|
|
547
658
|
const h = dt / steps;
|
|
548
659
|
list.forEach((r) => {
|
|
549
660
|
const rr = r.el.getBoundingClientRect();
|
|
@@ -567,8 +678,13 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
567
678
|
const rgt = Math.max(tgt[2], sp.e[2]) - sx, btm = Math.max(tgt[3], sp.e[3]) - sy;
|
|
568
679
|
r.drawn = { l, t, w: rgt - l, h: btm - t };
|
|
569
680
|
});
|
|
570
|
-
this.
|
|
681
|
+
this.drawnScroll = [scrollX, scrollY];
|
|
682
|
+
this.lastList = list.slice(0, this.max);
|
|
683
|
+
this.draw(this.lastList);
|
|
571
684
|
};
|
|
685
|
+
this.lastList = [];
|
|
686
|
+
this.oneShot = false;
|
|
687
|
+
this.rgbCache = /* @__PURE__ */ new Map();
|
|
572
688
|
this.canvas = canvas;
|
|
573
689
|
this.gl = gl;
|
|
574
690
|
this.settings = settings;
|
|
@@ -601,9 +717,11 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
601
717
|
this.mouse.x = this.mouse.tx = innerWidth / 2;
|
|
602
718
|
this.mouse.y = this.mouse.ty = innerHeight / 2;
|
|
603
719
|
addEventListener("resize", this.resize);
|
|
720
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
721
|
+
if (this.coarse) addEventListener("scroll", this.onScroll, { passive: true });
|
|
604
722
|
addEventListener("pointermove", this.onPointer, { passive: true });
|
|
605
723
|
document.addEventListener("pointerleave", this.onLeave);
|
|
606
|
-
this.
|
|
724
|
+
this.applyResize();
|
|
607
725
|
this.raf = requestAnimationFrame(this.frame);
|
|
608
726
|
}
|
|
609
727
|
/** Returns null when WebGL2 is unavailable. */
|
|
@@ -710,6 +828,8 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
710
828
|
removing: false,
|
|
711
829
|
lx: 0,
|
|
712
830
|
ly: 0,
|
|
831
|
+
leanCss: "",
|
|
832
|
+
scaleCss: "",
|
|
713
833
|
joined: false,
|
|
714
834
|
dragging: false,
|
|
715
835
|
layoutBox: null,
|
|
@@ -754,7 +874,7 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
754
874
|
}
|
|
755
875
|
layoutBoxOf(r) {
|
|
756
876
|
if (r.layoutBox) return r.layoutBox();
|
|
757
|
-
const b = elementBox(r.el);
|
|
877
|
+
const b = r.box ?? elementBox(r.el);
|
|
758
878
|
return { l: b.l - r.lx, t: b.t - r.ly, w: b.w, h: b.h };
|
|
759
879
|
}
|
|
760
880
|
/** Send a pulse through the material from a viewport point. */
|
|
@@ -776,6 +896,9 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
776
896
|
destroy() {
|
|
777
897
|
cancelAnimationFrame(this.raf);
|
|
778
898
|
removeEventListener("resize", this.resize);
|
|
899
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
900
|
+
removeEventListener("scroll", this.onScroll);
|
|
901
|
+
clearTimeout(this.scrollIdle);
|
|
779
902
|
removeEventListener("pointermove", this.onPointer);
|
|
780
903
|
document.removeEventListener("pointerleave", this.onLeave);
|
|
781
904
|
this.recs.forEach((r) => {
|
|
@@ -785,6 +908,27 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
785
908
|
this.recs.clear();
|
|
786
909
|
this.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
787
910
|
}
|
|
911
|
+
freeze() {
|
|
912
|
+
const c = this.canvas;
|
|
913
|
+
cancelAnimationFrame(this.raf);
|
|
914
|
+
this.raf = 0;
|
|
915
|
+
const vw = innerWidth, vh = innerHeight, runway = Math.round(vh * _PlasmaRenderer.RUNWAY);
|
|
916
|
+
this.region = { ox: 0, oy: runway, w: vw, h: vh + 2 * runway };
|
|
917
|
+
this.allocate();
|
|
918
|
+
this.oneShot = true;
|
|
919
|
+
this.frame(performance.now());
|
|
920
|
+
this.oneShot = false;
|
|
921
|
+
c.style.inset = "";
|
|
922
|
+
c.style.width = `${this.region.w}px`;
|
|
923
|
+
c.style.height = `${this.region.h}px`;
|
|
924
|
+
c.style.position = "absolute";
|
|
925
|
+
c.style.top = "0px";
|
|
926
|
+
c.style.left = "0px";
|
|
927
|
+
const r = c.getBoundingClientRect();
|
|
928
|
+
c.style.top = `${-r.top - (scrollY - this.drawnScroll[1]) - runway}px`;
|
|
929
|
+
c.style.left = `${-r.left - (scrollX - this.drawnScroll[0])}px`;
|
|
930
|
+
this.frozen = true;
|
|
931
|
+
}
|
|
788
932
|
isLight() {
|
|
789
933
|
const t = this.settings.theme;
|
|
790
934
|
if (t !== "auto") return t === "light";
|
|
@@ -793,11 +937,11 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
793
937
|
return !!this.lightQuery?.matches;
|
|
794
938
|
}
|
|
795
939
|
draw(list) {
|
|
796
|
-
const gl = this.gl, s = this.settings;
|
|
940
|
+
const gl = this.gl, s = this.settings, rg = this.region;
|
|
797
941
|
const drawn = list.map((r) => r.drawn ?? elementBox(r.el));
|
|
798
942
|
list.forEach((r, i) => {
|
|
799
943
|
const b = drawn[i];
|
|
800
|
-
this.P.set([b.l + b.w / 2, b.t + b.h / 2, b.w / 2, b.h / 2], i * 4);
|
|
944
|
+
this.P.set([b.l + b.w / 2 + rg.ox, b.t + b.h / 2 + rg.oy, b.w / 2, b.h / 2], i * 4);
|
|
801
945
|
const squareAgainst = r.fuse === false ? [] : drawn.filter((_, j) => j !== i && list[j].fuse !== false);
|
|
802
946
|
this.R.set(cornerRadii(b, r.radius, squareAgainst, -1), i * 4);
|
|
803
947
|
this.F[i] = Math.max(0, r.form);
|
|
@@ -810,10 +954,11 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
810
954
|
this.EL[i] = r.elevNow;
|
|
811
955
|
this.SOLO[i] = r.fuse === false ? 1 : 0;
|
|
812
956
|
});
|
|
813
|
-
this.pulses.forEach((p, i) => this.RP.set(p, i * 4));
|
|
957
|
+
this.pulses.forEach((p, i) => this.RP.set([p[0] + rg.ox, p[1] + rg.oy, p[2], p[3]], i * 4));
|
|
814
958
|
const light = this.isLight() ? 1 : 0;
|
|
815
959
|
const setCommon = (u, scale) => {
|
|
816
|
-
gl.uniform2f(u.uRes,
|
|
960
|
+
gl.uniform2f(u.uRes, rg.w, rg.h);
|
|
961
|
+
gl.uniform4f(u.uView, rg.ox, rg.oy, innerWidth, innerHeight);
|
|
817
962
|
gl.uniform1f(u.uScale, scale);
|
|
818
963
|
gl.uniform1f(u.uTime, this.time);
|
|
819
964
|
gl.uniform1f(u.uGoo, this.blend);
|
|
@@ -822,10 +967,10 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
822
967
|
gl.uniform1f(u.uMouseAmt, this.mouse.amt);
|
|
823
968
|
gl.uniform1f(u.uDropR, s.pointerDrop ? 15 : 0);
|
|
824
969
|
gl.uniform1f(u.uAmbient, s.ambientDrops ? 1 : 0);
|
|
825
|
-
gl.uniform1f(u.uScroll, scrollY * 0.25);
|
|
970
|
+
gl.uniform1f(u.uScroll, this.coarse && s.freezeOnScroll ? scrollY - rg.oy : scrollY * 0.25);
|
|
826
971
|
gl.uniform1f(u.uVisc, Math.min(Math.max(s.viscosity, 0), 1));
|
|
827
972
|
gl.uniform1f(u.uFlow, Math.max(s.flow, 0));
|
|
828
|
-
gl.uniform2f(u.uMouse, this.mouse.x, this.mouse.y);
|
|
973
|
+
gl.uniform2f(u.uMouse, this.mouse.x + rg.ox, this.mouse.y + rg.oy);
|
|
829
974
|
gl.uniform4fv(u.uP, this.P);
|
|
830
975
|
gl.uniform4fv(u.uR, this.R);
|
|
831
976
|
gl.uniform1fv(u.uF, this.F);
|
|
@@ -879,14 +1024,20 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
879
1024
|
gl.useProgram(bl.pr);
|
|
880
1025
|
gl.uniform1i(bl.u.uTex, 0);
|
|
881
1026
|
gl.activeTexture(gl.TEXTURE0);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1027
|
+
const n = list.length;
|
|
1028
|
+
let hasFrost = false, hasTint = false, hasElev = false;
|
|
1029
|
+
for (let i = 0; i < n; i++) {
|
|
1030
|
+
if (this.FR[i] > 2e-3) hasFrost = true;
|
|
1031
|
+
if (this.T[i * 4 + 3] > 2e-3) hasTint = true;
|
|
1032
|
+
if (this.EL[i] > 2e-3) hasElev = true;
|
|
1033
|
+
}
|
|
1034
|
+
if (hasFrost) {
|
|
1035
|
+
pass(this.rtBg, this.rtB, 1.5 * k, 0);
|
|
1036
|
+
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
1037
|
+
pass(this.rtBgM, this.rtB, 2.5 * k, 0);
|
|
1038
|
+
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
1039
|
+
pass(this.rtBgM, this.rtB, 2.5 * k, 0);
|
|
1040
|
+
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
890
1041
|
pass(this.rtBgM, this.rtB, 5 * k, 0);
|
|
891
1042
|
pass(this.rtB, this.rtBgH, 0, 5 * k);
|
|
892
1043
|
pass(this.rtBgH, this.rtB, 5 * k, 0);
|
|
@@ -907,21 +1058,21 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
907
1058
|
gl.uniform1i(bl.u.uTex, 0);
|
|
908
1059
|
gl.activeTexture(gl.TEXTURE0);
|
|
909
1060
|
const so = 1.4 * k * s.smoothness;
|
|
910
|
-
for (let
|
|
1061
|
+
for (let i = 0; i < 3; i++) {
|
|
911
1062
|
pass(this.rtA, this.rtB, so, 0);
|
|
912
1063
|
pass(this.rtB, this.rtA, 0, so);
|
|
913
1064
|
}
|
|
914
|
-
for (let
|
|
1065
|
+
if (hasTint) for (let i = 0; i < 3; i++) {
|
|
915
1066
|
pass(this.rtT, this.rtB, so, 0);
|
|
916
1067
|
pass(this.rtB, this.rtT, 0, so);
|
|
917
1068
|
}
|
|
918
|
-
for (let
|
|
1069
|
+
if (hasFrost || hasElev) for (let i = 0; i < 3; i++) {
|
|
919
1070
|
pass(this.rtFr, this.rtB, so, 0);
|
|
920
1071
|
pass(this.rtB, this.rtFr, 0, so);
|
|
921
1072
|
}
|
|
922
1073
|
pass(this.rtA, this.rtB, 2 * k, 0);
|
|
923
1074
|
pass(this.rtB, this.rtC, 0, 2 * k);
|
|
924
|
-
for (let
|
|
1075
|
+
for (let i = 0; i < 3; i++) {
|
|
925
1076
|
pass(this.rtC, this.rtB, 2 * k, 0);
|
|
926
1077
|
pass(this.rtB, this.rtC, 0, 2 * k);
|
|
927
1078
|
}
|
|
@@ -964,6 +1115,14 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
964
1115
|
gl.activeTexture(gl.TEXTURE0);
|
|
965
1116
|
}
|
|
966
1117
|
rgb(hex) {
|
|
1118
|
+
let v = this.rgbCache.get(hex);
|
|
1119
|
+
if (!v) {
|
|
1120
|
+
v = this.rgbParse(hex);
|
|
1121
|
+
this.rgbCache.set(hex, v);
|
|
1122
|
+
}
|
|
1123
|
+
return v;
|
|
1124
|
+
}
|
|
1125
|
+
rgbParse(hex) {
|
|
967
1126
|
let c = this.tintCache.get(hex);
|
|
968
1127
|
if (!c) {
|
|
969
1128
|
c = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex) ? hexToRgb(hex) : [1, 1, 1];
|
|
@@ -1003,6 +1162,8 @@ var PlasmaRenderer = class _PlasmaRenderer {
|
|
|
1003
1162
|
return { tex, fb: gl.createFramebuffer(), w: 0, h: 0 };
|
|
1004
1163
|
}
|
|
1005
1164
|
};
|
|
1165
|
+
_PlasmaRenderer.RUNWAY = 1;
|
|
1166
|
+
var PlasmaRenderer = _PlasmaRenderer;
|
|
1006
1167
|
|
|
1007
1168
|
// src/PlasmaProvider.tsx
|
|
1008
1169
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1024,15 +1185,15 @@ var PlasmaContext = createContext({
|
|
|
1024
1185
|
});
|
|
1025
1186
|
var usePlasma = () => useContext(PlasmaContext);
|
|
1026
1187
|
var FALLBACK_CSS = `
|
|
1027
|
-
.plasma-
|
|
1188
|
+
.plasma-panel{box-sizing:border-box}
|
|
1028
1189
|
.plasma-fallback{
|
|
1029
1190
|
background:linear-gradient(160deg,rgb(255 255 255/.14),rgb(255 255 255/.05));
|
|
1030
1191
|
-webkit-backdrop-filter:blur(18px) saturate(160%);backdrop-filter:blur(18px) saturate(160%);
|
|
1031
1192
|
border:1px solid rgb(255 255 255/.22);
|
|
1032
1193
|
box-shadow:inset 0 1px 0 rgb(255 255 255/.25),0 20px 40px -20px rgb(0 0 0/.5);
|
|
1033
1194
|
}
|
|
1034
|
-
.plasma-
|
|
1035
|
-
.plasma-
|
|
1195
|
+
.plasma-panel[data-plasma-draggable]{touch-action:none;cursor:grab}
|
|
1196
|
+
.plasma-panel[data-plasma-dragging]{cursor:grabbing;user-select:none}
|
|
1036
1197
|
`;
|
|
1037
1198
|
function useReducedMotion() {
|
|
1038
1199
|
const [reduced, setReduced] = useState(false);
|
|
@@ -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,
|
|
@@ -1483,7 +1647,7 @@ var Plasma = forwardRef(function Plasma2({
|
|
|
1483
1647
|
e.preventDefault();
|
|
1484
1648
|
settle({ x: dest.current.x + d[0], y: dest.current.y + d[1] });
|
|
1485
1649
|
};
|
|
1486
|
-
const classes = ["plasma-
|
|
1650
|
+
const classes = ["plasma-panel", plasma.supported ? "" : "plasma-fallback", className].filter(Boolean).join(" ");
|
|
1487
1651
|
return /* @__PURE__ */ jsx2(
|
|
1488
1652
|
Comp,
|
|
1489
1653
|
{
|
package/dist/renderer.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RendererSettings {
|
|
|
11
11
|
theme: "auto" | "light" | "dark";
|
|
12
12
|
quality: number;
|
|
13
13
|
reducedMotion: boolean;
|
|
14
|
+
/** Touch devices: pin the last frame to the page during a fling and resume when it stops. */
|
|
15
|
+
freezeOnScroll: boolean;
|
|
14
16
|
/** Rim color: "iridescent", "tint" (each surface's tint), or a hex color. */
|
|
15
17
|
rimColor: string;
|
|
16
18
|
/** Rim width multiplier. */
|
|
@@ -25,7 +27,7 @@ export interface RendererSettings {
|
|
|
25
27
|
stretch: number;
|
|
26
28
|
/** Slow ripple along the outline. 0 = still edges. */
|
|
27
29
|
flow: number;
|
|
28
|
-
/** Default
|
|
30
|
+
/** Default plasma tint (hex). */
|
|
29
31
|
tint: string;
|
|
30
32
|
/** Default tint strength, 0 (clear) to 1 (solid color). */
|
|
31
33
|
opacity: number;
|
|
@@ -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/dist/shaders.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Default maximum
|
|
1
|
+
/** Default maximum plasma shapes drawn at once (offscreen shapes are culled first). */
|
|
2
2
|
export declare const DEFAULT_MAX_SHAPES = 16;
|
|
3
3
|
/** Maximum concurrent pulses. */
|
|
4
4
|
export declare const MAX_PULSES = 4;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxgarden/plasma-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"cruxgarden",
|
|
54
54
|
"react",
|
|
55
55
|
"webgl",
|
|
56
|
-
"
|
|
56
|
+
"panels",
|
|
57
57
|
"ui",
|
|
58
58
|
"animation"
|
|
59
59
|
],
|