@cruxgarden/plasma-ui 0.3.0 → 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 +26 -0
- package/README.md +80 -2
- package/dist/Plasma.d.ts +4 -0
- package/dist/PlasmaProvider.d.ts +46 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +688 -12
- package/dist/renderer.d.ts +55 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
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
|
+
|
|
5
31
|
### Every look the material adds is now a control
|
|
6
32
|
|
|
7
33
|
Six things gave the plasma a look of its own and only two could be turned off,
|
package/README.md
CHANGED
|
@@ -64,6 +64,19 @@ export function App() {
|
|
|
64
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
65
|
| `grain` | `number` | `1` | Film grain over the background (never over the surfaces); `0` turns it off |
|
|
66
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 |
|
|
67
80
|
| `pointerDrop` | `boolean` | `true` | Liquid drop that follows the pointer |
|
|
68
81
|
| `ambientDrops` | `boolean` | `false` | Decorative orbiting drops |
|
|
69
82
|
| `grid`, `magnet` | `number` | `24`, `40` | Snap grid size and edge latch distance |
|
|
@@ -92,6 +105,7 @@ Accepts all HTML attributes, plus the following:
|
|
|
92
105
|
| `offset` / `defaultOffset` | `{ x, y }` | | Controlled or initial offset; changes spring into place |
|
|
93
106
|
| `onDragStart`, `onDragEnd(offset)` | | | Drag lifecycle; `onDragEnd` gets the settled offset |
|
|
94
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` |
|
|
95
109
|
|
|
96
110
|
Everything else you pass goes to the rendered element. `PlasmaProps<C>` is the
|
|
97
111
|
full prop type for `<Plasma as={C}>`; `PlasmaOwnProps` is just the table above,
|
|
@@ -129,6 +143,69 @@ It takes `className`, `style` and `zIndex`. Note that this places and styles
|
|
|
129
143
|
the **element**; the renderer still draws the whole viewport. Confining the
|
|
130
144
|
field to a container is [roadmap](#roadmap) work, not something this prop does.
|
|
131
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
|
+
|
|
132
209
|
## Clear as water
|
|
133
210
|
|
|
134
211
|
Six things give the material a look of its own, and each is a separate
|
|
@@ -202,13 +279,14 @@ NOTE: `flow` ripples the outline, so leave it at `0` whenever flush edges should
|
|
|
202
279
|
- Place surfaces together or further apart than the Blend distance. Smaller gaps render as liquid bridging them.
|
|
203
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.
|
|
204
281
|
- Lean and Pulse use the CSS `translate` and `scale` properties, and Drag uses `transform`. Avoid setting these properties on `Plasma` elements yourself.
|
|
205
|
-
- `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.
|
|
206
284
|
|
|
207
285
|
## Roadmap
|
|
208
286
|
|
|
209
287
|
Ordered by priority:
|
|
210
288
|
|
|
211
|
-
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.
|
|
212
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.
|
|
213
291
|
3. **Scroll clipping** - plasma confined to scrollable containers.
|
|
214
292
|
4. **Pluggable Backgrounds** - colors, images, and live canvas/video shipped in 0.1 (`background` prop); custom shaders are next.
|
package/dist/Plasma.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export interface PlasmaOwnProps {
|
|
|
47
47
|
onDragEnd?: (offset: Offset) => void;
|
|
48
48
|
/** Fires when the surface fuses with or separates from a neighbor. */
|
|
49
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;
|
|
50
54
|
}
|
|
51
55
|
/**
|
|
52
56
|
* Props for `<Plasma as={C}>`: Plasma's own, plus everything `C` accepts.
|
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;
|
|
@@ -40,8 +40,52 @@ export interface PlasmaProviderProps {
|
|
|
40
40
|
wash?: number;
|
|
41
41
|
/** Film grain over the background (never over the surfaces). 0 turns it off. Default 1. */
|
|
42
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;
|
|
43
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. */
|
|
44
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;
|
|
45
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. */
|
|
46
90
|
viscosity?: number;
|
|
47
91
|
/** How far the plasma trails and stretches behind moving panels. 0 turns it off. Default 1. */
|
|
@@ -148,4 +192,4 @@ export interface PlasmaCanvasProps {
|
|
|
148
192
|
* supported yet.
|
|
149
193
|
*/
|
|
150
194
|
export declare function PlasmaCanvas({ className, style, zIndex }: PlasmaCanvasProps): React.JSX.Element;
|
|
151
|
-
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, pointerDrop, ambientDrops, grid, magnet, quality, maxSurfaces, zIndex, freezeOnScroll, canvas, }: PlasmaProviderProps): 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
|
@@ -18,3 +18,6 @@ export { PlasmaRenderer } from "./renderer";
|
|
|
18
18
|
export { makeShaders, DEFAULT_MAX_SHAPES } from "./shaders";
|
|
19
19
|
/** @internal */
|
|
20
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";
|
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 vec4 uView; 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, uTension, uThick;
|
|
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;
|
|
@@ -128,6 +128,7 @@ uniform vec2 uImgRes;
|
|
|
128
128
|
uniform float uHasImg;
|
|
129
129
|
uniform vec3 uBgColor;
|
|
130
130
|
uniform float uBgSolid;
|
|
131
|
+
uniform float uClear;
|
|
131
132
|
out vec4 o;
|
|
132
133
|
vec3 bg(vec2 p){
|
|
133
134
|
p.y += uScroll;
|
|
@@ -161,6 +162,15 @@ void main(){
|
|
|
161
162
|
o = vec4(uBgColor * (1. + shade), 1.);
|
|
162
163
|
return;
|
|
163
164
|
}
|
|
165
|
+
if (uHasImg > .5 && uClear > .5) {
|
|
166
|
+
// a clear ground samples what is beneath it exactly - no swirl, no warp -
|
|
167
|
+
// so a refraction of it lines up with the page it sits on
|
|
168
|
+
float sc = max(uView.z / uImgRes.x, uView.w / uImgRes.y);
|
|
169
|
+
vec2 uv = (p - uView.xy - .5*uView.zw) / (uImgRes * sc) + .5;
|
|
170
|
+
o = vec4(texture(uImg, clamp(uv, 0., 1.)).rgb, 1.);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (uClear > .5) { o = vec4(0., 0., 0., 1.); return; }
|
|
164
174
|
if (uHasImg > .5) {
|
|
165
175
|
// image background: slow swirl plus the pulse warp above
|
|
166
176
|
vec2 q = bp;
|
|
@@ -179,12 +189,38 @@ void main(){
|
|
|
179
189
|
${common}
|
|
180
190
|
uniform sampler2D uH, uS, uTint, uBg, uBgM, uBgH, uFrost;
|
|
181
191
|
uniform float uRefract, uDisp, uRim, uRimMode, uRimWidth, uSpec, uHair, uShim, uGlow, uWash, uGrain;
|
|
192
|
+
// Which material the surfaces are made of, and the one light every opaque one
|
|
193
|
+
// reads. Glass fakes its lighting off the pointer because you see through it;
|
|
194
|
+
// the moment anything is opaque, two materials disagreeing about where the sun
|
|
195
|
+
// is looks broken in a way no single shader can fix. So there is one light.
|
|
196
|
+
uniform float uMat;
|
|
197
|
+
// 1 when the ground is clear: alpha is the surface's coverage (and its
|
|
198
|
+
// shadow's darkness), so the canvas can sit above other content.
|
|
199
|
+
uniform float uClear;
|
|
200
|
+
uniform vec3 uLightDir;
|
|
201
|
+
// Surface finish, shared by every material that has one: 0 is a mirror, 1 is
|
|
202
|
+
// chalk. Anisotropy stretches the highlight along the grain \u2014 brushed metal
|
|
203
|
+
// and varnished wood both need it, and neither reads right without it.
|
|
204
|
+
uniform float uRough, uAniso;
|
|
205
|
+
// How thick a panel is, in CSS px, and how much the material rounds itself
|
|
206
|
+
// against that. Mercury has enormous surface tension, so a bead pulls toward
|
|
207
|
+
// a sphere; tension drives both the corner radius and how eagerly two beads
|
|
208
|
+
// merge into one body.
|
|
209
|
+
// The edge treatment. A rounded rectangle is right for a liquid, and wrong for
|
|
210
|
+
// almost everything else: stone chips, cloud billows, metal is cut. These
|
|
211
|
+
// displace the silhouette itself \u2014 amplitude in CSS px, scale in cycles \u2014
|
|
212
|
+
// before anything decides what is inside, so the outline, the rim, the shadow
|
|
213
|
+
// and the material all agree about where the panel ends.
|
|
214
|
+
uniform float uEdge, uEdgeScale, uEdgeSharp;
|
|
182
215
|
uniform vec3 uRimColor;
|
|
183
216
|
out vec4 o;
|
|
184
217
|
|
|
185
218
|
vec3 pal(float t){ return .5 + .5*cos(6.2831*(t + vec3(0., .33, .67))); }
|
|
186
219
|
float H(vec2 uv){ return texture(uH, uv).r; }
|
|
187
|
-
|
|
220
|
+
// Clamped, because a reflection offset is far larger than a refraction one:
|
|
221
|
+
// metal sweeps up to 150px and walks straight off the texture, where
|
|
222
|
+
// CLAMP_TO_EDGE smears the last row of pixels into arcs across the panel.
|
|
223
|
+
vec2 uvAt(vec2 q){ return clamp(vec2(q.x, uRes.y - q.y) / uRes, vec2(.001), vec2(.999)); }
|
|
188
224
|
// background seen through plasma with frost f: sharp, then medium, then heavy blur
|
|
189
225
|
vec3 seen(vec2 q, float f){
|
|
190
226
|
vec2 u = uvAt(q);
|
|
@@ -193,6 +229,248 @@ vec3 seen(vec2 q, float f){
|
|
|
193
229
|
vec3 soft = mix(texture(uBgM, u).rgb, texture(uBgH, u).rgb, smoothstep(.45, 1., f));
|
|
194
230
|
return mix(sharp, soft, smoothstep(0., .45, f));
|
|
195
231
|
}
|
|
232
|
+
// \u2500\u2500 Shading kit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
233
|
+
// A real microfacet BRDF, not pow(ndl, n). GGX + height-correlated Smith +
|
|
234
|
+
// Schlick is what every modern renderer uses, and it is the single biggest
|
|
235
|
+
// difference between a material that reads as itself and one that reads as
|
|
236
|
+
// coloured plastic.
|
|
237
|
+
float D_GGX(float ndh, float a){ float a2 = a*a; float d = ndh*ndh*(a2-1.)+1.; return a2/(3.14159265*d*d + 1e-7); }
|
|
238
|
+
// Anisotropic GGX: the highlight stretches along a direction. This is the tell
|
|
239
|
+
// for wood and for brushed metal \u2014 an isotropic highlight on wood looks wrong
|
|
240
|
+
// even when nothing else does.
|
|
241
|
+
float D_GGXaniso(float ndh, float tdh, float bdh, float ax, float ay){
|
|
242
|
+
float d = tdh*tdh/(ax*ax) + bdh*bdh/(ay*ay) + ndh*ndh;
|
|
243
|
+
return 1./(3.14159265*ax*ay*d*d + 1e-7);
|
|
244
|
+
}
|
|
245
|
+
float V_SmithGGX(float ndv, float ndl, float a){
|
|
246
|
+
float a2 = a*a;
|
|
247
|
+
float gv = ndl*sqrt(ndv*ndv*(1.-a2)+a2);
|
|
248
|
+
float gl = ndv*sqrt(ndl*ndl*(1.-a2)+a2);
|
|
249
|
+
return .5/max(gv+gl, 1e-5);
|
|
250
|
+
}
|
|
251
|
+
vec3 F_Schlick(vec3 f0, float u){ return f0 + (1.-f0)*pow(clamp(1.-u,0.,1.), 5.); }
|
|
252
|
+
|
|
253
|
+
// Lighting has to happen in linear light. Doing it in gamma space is the
|
|
254
|
+
// oldest way to make a render look cheap: highlights clip early and midtones
|
|
255
|
+
// go chalky. sqrt/square is the standard cheap sRGB pair.
|
|
256
|
+
vec3 toLinear(vec3 c){ return c*c; }
|
|
257
|
+
vec3 toSrgb(vec3 c){ return sqrt(max(c, 0.)); }
|
|
258
|
+
// ACES filmic, fitted. Keeps a bright specular from flattening into white.
|
|
259
|
+
vec3 tonemap(vec3 x){ return clamp((x*(2.51*x+.03))/(x*(2.43*x+.59)+.14), 0., 1.); }
|
|
260
|
+
|
|
261
|
+
// Plain fbm reads as fractal mush. Warping the domain by another fbm is what
|
|
262
|
+
// makes grain and rock look grown rather than generated \u2014 the background
|
|
263
|
+
// shader already leans on this.
|
|
264
|
+
vec2 warp(vec2 q, float k){
|
|
265
|
+
return q + k*vec2(fbm(q + vec2(1.7, 9.2)), fbm(q + vec2(5.3, 2.8)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Metal only reads as metal when it reflects something with structure, and a
|
|
269
|
+
// page background is nearly uniform \u2014 reflect it alone and you get grey paint
|
|
270
|
+
// with a shiny rim. So reflective materials also see a small studio: a sky
|
|
271
|
+
// gradient, a horizon, and a sun in the same direction as the light. Roughness
|
|
272
|
+
// widens the sun and flattens the band, which is what a prefiltered
|
|
273
|
+
// environment map does for real.
|
|
274
|
+
vec3 envmap(vec3 R, float rough){
|
|
275
|
+
float h = clamp(R.y * .5 + .5, 0., 1.);
|
|
276
|
+
vec3 sky = mix(vec3(.62, .70, .84), vec3(.10, .13, .19), pow(1. - h, 1.5));
|
|
277
|
+
vec3 ground = vec3(.055, .05, .048);
|
|
278
|
+
vec3 c = mix(ground, sky, smoothstep(.40, .60, h));
|
|
279
|
+
vec3 L = normalize(uLightDir);
|
|
280
|
+
float sun = pow(max(dot(R, L), 0.), mix(260., 5., rough));
|
|
281
|
+
c += vec3(1., .96, .88) * sun * (1. - rough * .7) * 2.2;
|
|
282
|
+
// the horizon band is what gives a curved metal edge its sweep
|
|
283
|
+
c += vec3(.75, .82, .95) * (1. - smoothstep(.0, .22, abs(h - .54))) * .45 * (1. - rough * .55);
|
|
284
|
+
return c;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// \u2500\u2500 The scene as a solid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
288
|
+
// Everything above shades a flat card: a 2D silhouette, a perturbed normal,
|
|
289
|
+
// a texture lookup. That is skinning, and it is why wood came out looking
|
|
290
|
+
// like a wood-effect laminate. A material reads as a material when there is a
|
|
291
|
+
// body to light \u2014 so the same shapes are extruded into a slab and the view ray
|
|
292
|
+
// is marched through it, the way a game would.
|
|
293
|
+
//
|
|
294
|
+
// The 2D mask is a free conservative bound: outside it there is nothing to
|
|
295
|
+
// hit, so the march never starts.
|
|
296
|
+
float map3(vec3 q, float tension, float thick){
|
|
297
|
+
float d = 1e5;
|
|
298
|
+
for (int i = 0; i < uCount; i++) {
|
|
299
|
+
float f = uF[i];
|
|
300
|
+
if (f < .005) continue;
|
|
301
|
+
vec2 c = uP[i].xy;
|
|
302
|
+
vec2 h = uP[i].zw * f;
|
|
303
|
+
float rmax = max(max(uR[i].x, uR[i].y), max(uR[i].z, uR[i].w));
|
|
304
|
+
// The footprint is the element's, exactly as plasma's is. Mercury is not
|
|
305
|
+
// a blob that swallows the layout \u2014 it is a panel, poured. Tension belongs
|
|
306
|
+
// to the edge and to the merge, not to the shape: pulling the whole
|
|
307
|
+
// footprint toward a disc threw the panel away and left content floating
|
|
308
|
+
// over nothing.
|
|
309
|
+
float d2 = sdBoxG(q.xy - c, h, min(uR[i], vec4(min(h.x, h.y))), rmax).x;
|
|
310
|
+
// Extrude, with the rim rolled over so the slab has a shoulder rather than
|
|
311
|
+
// a cut edge. Tension fattens that roll \u2014 where a bead of mercury reads as
|
|
312
|
+
// poured is its edge, not its outline.
|
|
313
|
+
float roll = thick * (.22 + .50 * tension);
|
|
314
|
+
vec2 w = vec2(d2 + roll, abs(q.z) - (thick - roll));
|
|
315
|
+
float de = min(max(w.x, w.y), 0.) + length(max(w, 0.)) - roll;
|
|
316
|
+
d = (i == 0) ? de : smin(d, de, uGoo * (.6 + 1.8 * tension));
|
|
317
|
+
}
|
|
318
|
+
// Two slow swells across the whole body, displacing the surface itself
|
|
319
|
+
// rather than tilting a normal \u2014 the march finds a moving surface, so the
|
|
320
|
+
// reflection travels over real waves. This is what separates water from a
|
|
321
|
+
// picture of water.
|
|
322
|
+
// Displacing a distance field costs it the property the march depends on:
|
|
323
|
+
// d is no longer a safe distance, so a full step overshoots the surface and
|
|
324
|
+
// the ray lands inside, which is the mottled crust this produced at first.
|
|
325
|
+
// The amplitude is kept small and the march below steps at a fraction, which
|
|
326
|
+
// is the standard price of displacement.
|
|
327
|
+
if (tension > .01) {
|
|
328
|
+
float wv = (fbm(q.xy * .0075 + vec2(uTime * .11, uTime * .06)) - .5) * 1.6
|
|
329
|
+
+ (fbm(q.xy * .019 - vec2(uTime * .08, uTime * .13)) - .5) * .6;
|
|
330
|
+
d -= wv * thick * .07 * tension;
|
|
331
|
+
}
|
|
332
|
+
return d;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A crystalline crown: the panel's face is a regular triangular lattice, and
|
|
337
|
+
* every triangle is a flat facet with its own plane.
|
|
338
|
+
*
|
|
339
|
+
* Regular, not random \u2014 a Delaunay scatter reads as shattered safety glass,
|
|
340
|
+
* and a lattice reads as something cut. The heights at the lattice corners are
|
|
341
|
+
* hashed, so the facets tilt differently while the pattern stays symmetric.
|
|
342
|
+
*
|
|
343
|
+
* The normal comes out exact rather than differenced, which is what makes the
|
|
344
|
+
* faces crisp: a facet is a plane, so its normal is constant across it, and
|
|
345
|
+
* there is nothing for a march to stipple.
|
|
346
|
+
*/
|
|
347
|
+
vec3 facetPlane(vec2 uv, float amp, out float height){
|
|
348
|
+
vec2 c = floor(uv), f = fract(uv);
|
|
349
|
+
float h00 = hash(c) * amp;
|
|
350
|
+
float h10 = hash(c + vec2(1., 0.)) * amp;
|
|
351
|
+
float h01 = hash(c + vec2(0., 1.)) * amp;
|
|
352
|
+
float h11 = hash(c + vec2(1., 1.)) * amp;
|
|
353
|
+
float dx, dy;
|
|
354
|
+
// Which way the cell's diagonal runs is decided per cell. One diagonal
|
|
355
|
+
// everywhere reads as corduroy; alternating reads as cut.
|
|
356
|
+
bool flip = hash(c + 91.7) > .5;
|
|
357
|
+
bool lower = flip ? (f.x + f.y < 1.) : (f.x > f.y);
|
|
358
|
+
if (lower) {
|
|
359
|
+
height = h00 + (h10 - h00) * f.x + (h01 - h00) * f.y;
|
|
360
|
+
dx = h10 - h00; dy = h01 - h00;
|
|
361
|
+
} else {
|
|
362
|
+
height = h11 + (h01 - h11) * (1. - f.x) + (h10 - h11) * (1. - f.y);
|
|
363
|
+
dx = h11 - h01; dy = h11 - h10;
|
|
364
|
+
}
|
|
365
|
+
return normalize(vec3(-dx, -dy, 1.));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The crystal's top surface, as a height in page px, plus the exact plane
|
|
370
|
+
* normal of whichever facet the point lies on. This is a real surface: the
|
|
371
|
+
* march below has to find where the ray crosses it, so the facets occlude one
|
|
372
|
+
* another, break the silhouette, and shadow each other. A normal returned
|
|
373
|
+
* without an intersection would be a normal map, which is what the version
|
|
374
|
+
* before this was.
|
|
375
|
+
*/
|
|
376
|
+
float crownZ(vec2 xy, float gcut, float amp, float thick, out vec3 nrm){
|
|
377
|
+
float h;
|
|
378
|
+
nrm = facetPlane(xy / gcut, amp / gcut, h);
|
|
379
|
+
return thick + h * gcut;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// \u2500\u2500 The gem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
383
|
+
// Facet directions, spread over a hemisphere and mirrored, standing in for a
|
|
384
|
+
// cut stone's crown and pavilion. Generated rather than tabulated so the count
|
|
385
|
+
// is a constant to change, not a table to rewrite.
|
|
386
|
+
vec3 facetN(int i){
|
|
387
|
+
float fi = float(i);
|
|
388
|
+
float a = fi * 2.3999632; // golden angle, so they do not band
|
|
389
|
+
float z = mix(.22, .92, fract(fi * .6180339));
|
|
390
|
+
float r = sqrt(max(0., 1. - z * z));
|
|
391
|
+
return normalize(vec3(cos(a) * r, sin(a) * r, z));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The panel as a piece of gem. The slab is the element's own footprint,
|
|
396
|
+
* extruded; the facets are half-spaces cut through it.
|
|
397
|
+
*
|
|
398
|
+
* The cuts are anchored in PAGE space, not the panel's, which is the whole
|
|
399
|
+
* trick: a facet plane runs on across the gap and slices the next panel too,
|
|
400
|
+
* so the panels read as pieces carved out of one stone rather than as
|
|
401
|
+
* separate gems that happen to match.
|
|
402
|
+
*/
|
|
403
|
+
float mapGem(vec3 q, float thick, float cut){
|
|
404
|
+
float d = 1e5;
|
|
405
|
+
for (int i = 0; i < uCount; i++) {
|
|
406
|
+
float f = uF[i];
|
|
407
|
+
if (f < .005) continue;
|
|
408
|
+
vec2 c = uP[i].xy;
|
|
409
|
+
vec2 h = uP[i].zw * f;
|
|
410
|
+
float rmax = max(max(uR[i].x, uR[i].y), max(uR[i].z, uR[i].w));
|
|
411
|
+
float d2 = sdBoxG(q.xy - c, h, min(uR[i], vec4(min(h.x, h.y))), rmax).x;
|
|
412
|
+
vec2 w = vec2(d2, abs(q.z) - thick);
|
|
413
|
+
float de = min(max(w.x, w.y), 0.) + length(max(w, 0.)) - 1.5;
|
|
414
|
+
d = (i == 0) ? de : min(d, de);
|
|
415
|
+
}
|
|
416
|
+
// Cut the stone. Each plane repeats on a lattice so the whole page is one
|
|
417
|
+
// crystal the panels are taken out of. Six directions, and the lattice is
|
|
418
|
+
// wide \u2014 a few facets across a panel, not hundreds. Cut it finely and a gem
|
|
419
|
+
// stops being a gem and becomes gravel, which is exactly what nine planes at
|
|
420
|
+
// 34px produced on the first try.
|
|
421
|
+
for (int i = 0; i < 6; i++) {
|
|
422
|
+
vec3 nn = facetN(i);
|
|
423
|
+
if (fract(float(i) * .5) > .25) nn.z = -nn.z; // mirror half of them below
|
|
424
|
+
// The plane sits near the far end of its lattice cell, so a cut trims a
|
|
425
|
+
// corner instead of slicing the body in half. Six planes at 70% of the
|
|
426
|
+
// cell removed most of the volume and left the panels as floating shards.
|
|
427
|
+
// A gem is faceted, not shattered.
|
|
428
|
+
float hgt = dot(q, nn);
|
|
429
|
+
float plane = floor(hgt / cut) * cut + cut * .965;
|
|
430
|
+
d = max(d, hgt - plane);
|
|
431
|
+
}
|
|
432
|
+
return d;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
vec3 gemNormal(vec3 q, float thick, float cut){
|
|
436
|
+
vec2 e = vec2(.4, 0.);
|
|
437
|
+
return normalize(vec3(
|
|
438
|
+
mapGem(q + e.xyy, thick, cut) - mapGem(q - e.xyy, thick, cut),
|
|
439
|
+
mapGem(q + e.yxy, thick, cut) - mapGem(q - e.yxy, thick, cut),
|
|
440
|
+
mapGem(q + e.yyx, thick, cut) - mapGem(q - e.yyx, thick, cut)
|
|
441
|
+
));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
vec3 normal3(vec3 q, float tension, float thick){
|
|
445
|
+
vec2 e = vec2(.75, 0.);
|
|
446
|
+
return normalize(vec3(
|
|
447
|
+
map3(q + e.xyy, tension, thick) - map3(q - e.xyy, tension, thick),
|
|
448
|
+
map3(q + e.yxy, tension, thick) - map3(q - e.yxy, tension, thick),
|
|
449
|
+
map3(q + e.yyx, tension, thick) - map3(q - e.yyx, tension, thick)
|
|
450
|
+
));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Worley/Voronoi: nearest cell, second nearest, and the cell's id. The gap
|
|
454
|
+
// between the two is the distance to a cell wall, which is what draws the
|
|
455
|
+
// seams of a crystal without any geometry.
|
|
456
|
+
vec4 voronoi(vec2 q){
|
|
457
|
+
vec2 ip = floor(q), fp = fract(q);
|
|
458
|
+
float d1 = 8., d2 = 8.;
|
|
459
|
+
vec2 id = vec2(0.);
|
|
460
|
+
for (int j = -1; j <= 1; j++) {
|
|
461
|
+
for (int i = -1; i <= 1; i++) {
|
|
462
|
+
vec2 g = vec2(float(i), float(j));
|
|
463
|
+
vec2 cell = ip + g;
|
|
464
|
+
vec2 o = vec2(hash(cell), hash(cell + 31.7));
|
|
465
|
+
vec2 r = g + o - fp;
|
|
466
|
+
float d = dot(r, r);
|
|
467
|
+
if (d < d1) { d2 = d1; d1 = d; id = cell; }
|
|
468
|
+
else if (d < d2) { d2 = d; }
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return vec4(sqrt(d1), sqrt(d2), id);
|
|
472
|
+
}
|
|
473
|
+
|
|
196
474
|
// B-spline bicubic from four bilinear taps keeps curved outlines round.
|
|
197
475
|
float S(vec2 uv){
|
|
198
476
|
vec2 ts = vec2(textureSize(uS, 0));
|
|
@@ -219,9 +497,23 @@ void main(){
|
|
|
219
497
|
float s = S(uv);
|
|
220
498
|
vec2 gs = vec2(S(uv+vec2(tx.x,0.)) - S(uv-vec2(tx.x,0.)), S(uv+vec2(0.,tx.y)) - S(uv-vec2(0.,tx.y))) / (2.*cssPerTexel);
|
|
221
499
|
float sd = clamp((s - .5) / max(length(gs), 1e-3), -60., 60.);
|
|
500
|
+
if (uEdge > .01) {
|
|
501
|
+
// Two octaves: the first gives the large shape (billows, chips), the
|
|
502
|
+
// second the fine break-up. Warping keeps it from reading as a sine wave
|
|
503
|
+
// stamped around the outline.
|
|
504
|
+
vec2 ep = warp(p * uEdgeScale, .7);
|
|
505
|
+
float e1 = fbm(ep) - .5;
|
|
506
|
+
float e2 = fbm(ep * 3.3 + 17.) - .5;
|
|
507
|
+
float disp = e1 * 1.5 + e2 * .55;
|
|
508
|
+
// uEdgeSharp 0 leaves it rolling; 1 pushes it toward flats and points,
|
|
509
|
+
// which is what reads as chipped rather than melted.
|
|
510
|
+
disp = mix(disp, sign(disp) * pow(abs(disp) * 2., .55) * .5, clamp(uEdgeSharp, 0., 1.));
|
|
511
|
+
sd += disp * uEdge;
|
|
512
|
+
}
|
|
222
513
|
|
|
223
514
|
vec3 back = texture(uBg, uv).rgb;
|
|
224
515
|
float grain = 1.;
|
|
516
|
+
float cover = 0.;
|
|
225
517
|
|
|
226
518
|
// shadow: offset and strength follow the surface's elevation
|
|
227
519
|
float msk = max(texture(uS, uv).r, 1e-3);
|
|
@@ -281,17 +573,317 @@ void main(){
|
|
|
281
573
|
if (uRimMode > .5 && uRimMode < 1.5) rimCol = uRimColor * facing * 1.4;
|
|
282
574
|
else if (uRimMode > 1.5) rimCol = tcol * facing * 1.4;
|
|
283
575
|
|
|
576
|
+
// A 3D normal from the 2D height gradient: flat in the middle, turning
|
|
577
|
+
// outward and down across the bevel. Every material shades from this, and
|
|
578
|
+
// the opaque ones perturb it further with their own detail.
|
|
579
|
+
vec3 Nb = normalize(vec3(n * (.30 + .70 * bevel), max(.20, 1. - bevel)));
|
|
580
|
+
vec3 Ldir = normalize(uLightDir);
|
|
581
|
+
vec3 V = vec3(0., 0., 1.); // orthographic view, straight on
|
|
284
582
|
vec3 plasma = refr;
|
|
285
|
-
plasma += rimCol * fres * .45 * hl * uRim;
|
|
286
|
-
plasma += vec3(1.) * spec * .75 * hl * uSpec;
|
|
287
|
-
// faint shimmer across the body; fades out as the tint becomes opaque
|
|
288
|
-
plasma += pal(uTime*.04 + vp.y/900. + uEnergy*.3) * .05 * lift * (1.+uEnergy*2.) * hl * (1. - talpha) * uShim;
|
|
289
|
-
vec3 hairCol = uRimMode > .5 ? mix(vec3(1.), rimCol / 1.4, .6) : vec3(.9,.95,1.);
|
|
290
|
-
plasma += hairCol * (1.-smoothstep(0., 1.6, abs(sd - .7))) * .4 * hl * uHair;
|
|
291
|
-
|
|
292
583
|
float a = smoothstep(-.8, .8, sd);
|
|
584
|
+
|
|
585
|
+
if (uMat < .5) {
|
|
586
|
+
// \u2500\u2500 plasma \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
587
|
+
plasma += rimCol * fres * .45 * hl * uRim;
|
|
588
|
+
plasma += vec3(1.) * spec * .75 * hl * uSpec;
|
|
589
|
+
// faint shimmer across the body; fades out as the tint becomes opaque
|
|
590
|
+
plasma += pal(uTime*.04 + vp.y/900. + uEnergy*.3) * .05 * lift * (1.+uEnergy*2.) * hl * (1. - talpha) * uShim;
|
|
591
|
+
vec3 hairCol = uRimMode > .5 ? mix(vec3(1.), rimCol / 1.4, .6) : vec3(.9,.95,1.);
|
|
592
|
+
plasma += hairCol * (1.-smoothstep(0., 1.6, abs(sd - .7))) * .4 * hl * uHair;
|
|
593
|
+
} else if (uMat < 1.5) {
|
|
594
|
+
// \u2500\u2500 crystal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
595
|
+
// Actual 3D. The panel is a solid whose top is a cut crown \u2014 a lattice
|
|
596
|
+
// of flat facets, anchored in PAGE space so a facet runs on across the
|
|
597
|
+
// gap into the next panel. The view ray is marched until it crosses that
|
|
598
|
+
// surface, so the facets genuinely occlude each other, break the
|
|
599
|
+
// silhouette where they rise and fall, and shadow one another.
|
|
600
|
+
//
|
|
601
|
+
// A height field is marched by stepping and testing which side of the
|
|
602
|
+
// surface you are on, then bisecting \u2014 not by sphere tracing, which a
|
|
603
|
+
// piecewise-planar field is hostile to.
|
|
604
|
+
float gcut = max(uThick * 2.4, 52.);
|
|
605
|
+
float amp = gcut * .45;
|
|
606
|
+
float gthick = max(uThick, 10.);
|
|
607
|
+
|
|
608
|
+
vec3 gro = vec3(p, gthick + amp * 1.6 + 4.);
|
|
609
|
+
vec3 grd = normalize(vec3((p - uMouse) * .00055, -1.));
|
|
610
|
+
|
|
611
|
+
float tPrev = 0., tHit = -1.;
|
|
612
|
+
vec3 nHit = vec3(0., 0., 1.);
|
|
613
|
+
float stepLen = max(amp * .16, 1.2);
|
|
614
|
+
for (int i = 0; i < 64; i++) {
|
|
615
|
+
float t = float(i) * stepLen;
|
|
616
|
+
vec3 q = gro + grd * t;
|
|
617
|
+
// Outside the panel's footprint there is no stone to hit.
|
|
618
|
+
if (scene(q.xy) > 0.) { tPrev = t; continue; }
|
|
619
|
+
vec3 nn;
|
|
620
|
+
float sz = crownZ(q.xy, gcut, amp, gthick, nn);
|
|
621
|
+
if (q.z <= sz) {
|
|
622
|
+
// Crossed it. Bisect for the exact facet plane.
|
|
623
|
+
float lo = tPrev, hi = t;
|
|
624
|
+
for (int k = 0; k < 6; k++) {
|
|
625
|
+
float mid = (lo + hi) * .5;
|
|
626
|
+
vec3 qm = gro + grd * mid;
|
|
627
|
+
vec3 nm;
|
|
628
|
+
float szm = crownZ(qm.xy, gcut, amp, gthick, nm);
|
|
629
|
+
if (qm.z <= szm) { hi = mid; nHit = nm; } else { lo = mid; }
|
|
630
|
+
}
|
|
631
|
+
tHit = hi;
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
tPrev = t;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (tHit < 0.) {
|
|
638
|
+
a = 0.; // the ray passed over the stone
|
|
639
|
+
} else {
|
|
640
|
+
vec3 q0 = gro + grd * tHit;
|
|
641
|
+
vec3 Nf = nHit;
|
|
642
|
+
float ndv = max(dot(Nf, -grd), 1e-3);
|
|
643
|
+
float ndl = max(dot(Nf, Ldir), 0.);
|
|
644
|
+
vec3 H = normalize(Ldir - grd);
|
|
645
|
+
|
|
646
|
+
// Shadow: march back toward the light across the crown. This is the
|
|
647
|
+
// thing a normal map can never do \u2014 one facet standing in another's
|
|
648
|
+
// light.
|
|
649
|
+
float shade = 1.;
|
|
650
|
+
for (int i = 1; i <= 10; i++) {
|
|
651
|
+
vec3 qs = q0 + Ldir * (float(i) * amp * .30);
|
|
652
|
+
vec3 nn;
|
|
653
|
+
float sz = crownZ(qs.xy, gcut, amp, gthick, nn);
|
|
654
|
+
if (qs.z < sz - .5 && scene(qs.xy) < 0.) { shade = .25; break; }
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Through the stone: refract in, cross the body, refract out at the
|
|
658
|
+
// underside.
|
|
659
|
+
float ior = 1.85;
|
|
660
|
+
vec3 rin = refract(grd, Nf, 1. / ior);
|
|
661
|
+
vec3 nn2;
|
|
662
|
+
float h2;
|
|
663
|
+
vec3 Nb2 = facetPlane((q0.xy + rin.xy * gthick * 2.) / gcut + 23.7, amp / gcut, h2);
|
|
664
|
+
vec3 rout = refract(rin, -Nb2, ior);
|
|
665
|
+
if (dot(rout, rout) < 1e-6) rout = reflect(rin, -Nb2);
|
|
666
|
+
|
|
667
|
+
float disp = .06 * max(uDisp, .001) * 6.;
|
|
668
|
+
vec2 off2 = rout.xy * gthick * 4.;
|
|
669
|
+
vec3 thru = toLinear(vec3(
|
|
670
|
+
seen(p + off2 * (1. + disp), fr).r,
|
|
671
|
+
seen(p + off2, fr).g,
|
|
672
|
+
seen(p + off2 * (1. - disp), fr).b
|
|
673
|
+
));
|
|
674
|
+
|
|
675
|
+
vec3 body = toLinear(mix(vec3(.62, .48, .92), tcol, talpha));
|
|
676
|
+
vec3 lit = body * (.10 + 1.15 * ndl * shade);
|
|
677
|
+
lit += thru * body * 1.2 * (.25 + .75 * ndv);
|
|
678
|
+
|
|
679
|
+
vec3 F0 = vec3(.13);
|
|
680
|
+
float fres = pow(1. - ndv, 4.);
|
|
681
|
+
lit = mix(lit, envmap(reflect(grd, Nf), .02), F0.x + (1. - F0.x) * fres * .9);
|
|
682
|
+
|
|
683
|
+
float aa = .018 * .018;
|
|
684
|
+
lit += F_Schlick(F0, max(dot(H, -grd), 0.))
|
|
685
|
+
* D_GGX(max(dot(Nf, H), 0.), aa) * V_SmithGGX(ndv, ndl, aa) * ndl * 26. * uSpec * shade;
|
|
686
|
+
plasma = toSrgb(tonemap(lit));
|
|
687
|
+
a = 1.;
|
|
688
|
+
}
|
|
689
|
+
} else if (uMat < 3.5) {
|
|
690
|
+
bool merc = uMat > 2.5;
|
|
691
|
+
if (merc) {
|
|
692
|
+
// \u2500\u2500 mercury \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
693
|
+
// Marched, not painted. The ray enters the slab, and every normal
|
|
694
|
+
// below comes from the solid's own gradient \u2014 which is what makes a
|
|
695
|
+
// bead read as a bead instead of a rectangle with a chrome gradient
|
|
696
|
+
// on it. Surface tension does the rest: the footprint rounds toward a
|
|
697
|
+
// disc, and two panels within reach pull into one body with a neck.
|
|
698
|
+
float thick = max(uThick, 8.);
|
|
699
|
+
float tension = clamp(uTension, 0., 1.);
|
|
700
|
+
vec3 ro = vec3(p, thick * 3.5);
|
|
701
|
+
vec3 rd = normalize(vec3((p - uMouse) * .00035, -1.)); // a hair of perspective
|
|
702
|
+
float t = 0., hit = -1.;
|
|
703
|
+
for (int i = 0; i < 96; i++) {
|
|
704
|
+
vec3 q = ro + rd * t;
|
|
705
|
+
float dd = map3(q, tension, thick);
|
|
706
|
+
if (dd < .35) { hit = t; break; }
|
|
707
|
+
t += max(dd * .30, .25);
|
|
708
|
+
if (t > thick * 8.) break;
|
|
709
|
+
}
|
|
710
|
+
if (hit < 0.) {
|
|
711
|
+
a = 0.; // the march missed: leave the ground
|
|
712
|
+
} else {
|
|
713
|
+
vec3 q = ro + rd * hit;
|
|
714
|
+
vec3 Nm = normal3(q, tension, thick);
|
|
715
|
+
float ndv = max(dot(Nm, -rd), 1e-3);
|
|
716
|
+
float ndl = max(dot(Nm, Ldir), 0.);
|
|
717
|
+
vec3 H = normalize(Ldir - rd);
|
|
718
|
+
vec3 R = reflect(rd, Nm);
|
|
719
|
+
|
|
720
|
+
float rough = .04;
|
|
721
|
+
// A real reflection off a real normal, of the studio and of what is
|
|
722
|
+
// actually behind the bead.
|
|
723
|
+
vec2 roff = R.xy * 210.;
|
|
724
|
+
vec3 env = mix(envmap(R, rough), toLinear(seen(p - roff, rough)), .30);
|
|
725
|
+
vec3 F0 = mix(vec3(.86, .87, .90), toLinear(tcol), talpha);
|
|
726
|
+
vec3 Fr = F_Schlick(F0, ndv);
|
|
727
|
+
float aa = max(rough * rough, 1e-4);
|
|
728
|
+
vec3 sun = F_Schlick(F0, max(dot(H, -rd), 0.))
|
|
729
|
+
* D_GGX(max(dot(Nm, H), 0.), aa)
|
|
730
|
+
* V_SmithGGX(ndv, ndl, aa) * ndl;
|
|
731
|
+
vec3 me = env * Fr + sun * 16. * uSpec;
|
|
732
|
+
me += F0 * .03;
|
|
733
|
+
// Shimmer comes from the modelled swell moving under a sharp
|
|
734
|
+
// highlight, not from a noise field tinting the surface. The only
|
|
735
|
+
// colour added is thin-film interference, which is a function of the
|
|
736
|
+
// angle the geometry presents \u2014 no texture in it.
|
|
737
|
+
me += toLinear(pal(pow(1. - ndv, 1.6) * 1.4)) * .12 * uShim * pow(1. - ndv, .9);
|
|
738
|
+
plasma = toSrgb(tonemap(me));
|
|
739
|
+
// The silhouette is the march's, so a bead that has pulled away from
|
|
740
|
+
// the rectangle actually looks pulled away.
|
|
741
|
+
a = 1.;
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
// \u2500\u2500 metal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
745
|
+
// Milled, not poured: flat stock with a cut edge. Still the screen
|
|
746
|
+
// space path, which is honest for a flat sheet.
|
|
747
|
+
float rough = clamp(uRough, .04, .95);
|
|
748
|
+
vec3 Nm = Nb;
|
|
749
|
+
float aniso = uAniso;
|
|
750
|
+
if (aniso > .01) {
|
|
751
|
+
float brush = fbm(vec2(p.x * .9, p.y * 22.)) - .5;
|
|
752
|
+
Nm = normalize(Nm + vec3(0., brush * aniso * .55, 0.));
|
|
753
|
+
}
|
|
754
|
+
float mr = fbm(p * .035) - .5;
|
|
755
|
+
Nm = normalize(Nm + vec3(mr * .16, (fbm(p * .035 + 11.) - .5) * .16, 0.));
|
|
756
|
+
float ndv = max(dot(Nm, V), 1e-3);
|
|
757
|
+
float ndl = max(dot(Nm, Ldir), 0.);
|
|
758
|
+
vec3 H = normalize(Ldir + V);
|
|
759
|
+
vec3 R = reflect(-V, Nm);
|
|
760
|
+
vec2 roff = R.xy * pow(bevel, .9) * 150. * (1. - rough * .5);
|
|
761
|
+
vec3 env = mix(envmap(R, rough), toLinear(seen(p - roff, rough)), .22);
|
|
762
|
+
vec3 F0 = mix(vec3(.95, .93, .88), toLinear(tcol), talpha);
|
|
763
|
+
vec3 Fr = F_Schlick(F0, ndv);
|
|
764
|
+
float aa = max(rough * rough, 1e-3);
|
|
765
|
+
float D = aniso > .01
|
|
766
|
+
? D_GGXaniso(max(dot(Nm, H), 0.), dot(vec3(1.,0.,0.), H), dot(vec3(0.,1.,0.), H), aa * (1. + aniso * 3.), aa)
|
|
767
|
+
: D_GGX(max(dot(Nm, H), 0.), aa);
|
|
768
|
+
vec3 sun = F_Schlick(F0, max(dot(H, V), 0.)) * D * V_SmithGGX(ndv, ndl, aa) * ndl;
|
|
769
|
+
vec3 me = env * Fr + sun * 14. * uSpec;
|
|
770
|
+
me += F0 * .035;
|
|
771
|
+
plasma = toSrgb(tonemap(me));
|
|
772
|
+
}
|
|
773
|
+
} else if (uMat < 4.5) {
|
|
774
|
+
// \u2500\u2500 wood \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
775
|
+
// Grain lives in page coordinates, so a panel that resizes keeps its
|
|
776
|
+
// grain instead of stretching it \u2014 the first thing an opaque natural
|
|
777
|
+
// material asks for that glass never did.
|
|
778
|
+
vec2 wp = p * .0021;
|
|
779
|
+
// Rings are warped, then squeezed across one axis: straight fbm reads as
|
|
780
|
+
// camouflage, warped fbm reads as a board cut from a trunk.
|
|
781
|
+
vec2 wq = warp(vec2(wp.x * .40, wp.y * 4.2), .55);
|
|
782
|
+
float ring = fract(fbm(wq) * 2.05);
|
|
783
|
+
float band = abs(ring - .5) * 2.;
|
|
784
|
+
float pore = fbm(vec2(p.x * .18, p.y * .022));
|
|
785
|
+
float h0 = band * .7 + pore * .3;
|
|
786
|
+
// Detail normal by finite difference of that height, so the grain
|
|
787
|
+
// actually catches the light instead of being painted on.
|
|
788
|
+
vec2 e = vec2(1.4, 0.);
|
|
789
|
+
float hx = fract(fbm(warp(vec2((wp.x + e.x*.0021) * .40, wp.y * 4.2), .55)) * 2.05);
|
|
790
|
+
float hy = fract(fbm(warp(vec2(wp.x * .40, (wp.y + e.x*.0021) * 4.2), .55)) * 2.05);
|
|
791
|
+
hx = abs(hx - .5) * 2.; hy = abs(hy - .5) * 2.;
|
|
792
|
+
vec3 Nw = normalize(Nb + vec3((hx - band) * .85, (hy - band) * .85, 0.));
|
|
793
|
+
|
|
794
|
+
// Real boards are lower contrast than the first instinct: a wide dark-to
|
|
795
|
+
// -light sweep reads as charring, not as timber.
|
|
796
|
+
vec3 lightWood = toLinear(vec3(.62, .45, .28));
|
|
797
|
+
vec3 darkWood = toLinear(vec3(.40, .26, .145));
|
|
798
|
+
// A board is mostly one tone with the grain reading as a whisper over it.
|
|
799
|
+
vec3 alb = mix(darkWood, lightWood, smoothstep(.30, .70, band) * .55 + .22);
|
|
800
|
+
alb *= .95 + .09 * pore;
|
|
801
|
+
alb = mix(alb, toLinear(tcol), talpha * .6);
|
|
802
|
+
|
|
803
|
+
float ndv = max(dot(Nw, V), 1e-3);
|
|
804
|
+
float ndl = max(dot(Nw, Ldir), 0.);
|
|
805
|
+
vec3 H = normalize(Ldir + V);
|
|
806
|
+
// Anisotropic along the grain: a varnished board's highlight is a streak.
|
|
807
|
+
float aa = .34 * .34;
|
|
808
|
+
float D = D_GGXaniso(max(dot(Nw, H), 0.), dot(vec3(1.,0.,0.), H), dot(vec3(0.,1.,0.), H), aa * 4., aa);
|
|
809
|
+
vec3 F = F_Schlick(vec3(.045), max(dot(H, V), 0.));
|
|
810
|
+
vec3 wo = alb * (.16 + .95 * ndl);
|
|
811
|
+
wo += F * D * V_SmithGGX(ndv, ndl, aa) * ndl * 5.5 * uSpec;
|
|
812
|
+
wo += alb * pow(1. - ndv, 3.5) * .35;
|
|
813
|
+
plasma = toSrgb(tonemap(wo));
|
|
814
|
+
} else if (uMat < 5.5) {
|
|
815
|
+
// \u2500\u2500 stone \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
816
|
+
vec2 sq = warp(p * .022, .9);
|
|
817
|
+
float body = fbm(sq) * .62 + fbm(sq * 3.7) * .26 + fbm(sq * 11.) * .12;
|
|
818
|
+
float fleck = hash(floor(p * 2.2));
|
|
819
|
+
float vein = smoothstep(.52, .58, fbm(sq * .55 + 7.1));
|
|
820
|
+
float h0 = body + (fleck - .5) * .35;
|
|
821
|
+
float hx = fbm(warp((p + vec2(1.6, 0.)) * .022, .9)) + (hash(floor((p + vec2(1.6,0.)) * 2.2)) - .5) * .35;
|
|
822
|
+
float hy = fbm(warp((p + vec2(0., 1.6)) * .022, .9)) + (hash(floor((p + vec2(0.,1.6)) * 2.2)) - .5) * .35;
|
|
823
|
+
vec3 Ns = normalize(Nb + vec3((hx - h0) * 3.4, (hy - h0) * 3.4, 0.));
|
|
824
|
+
|
|
825
|
+
vec3 alb = mix(toLinear(vec3(.24, .235, .25)), toLinear(vec3(.55, .545, .55)), body);
|
|
826
|
+
alb = mix(alb, toLinear(vec3(.70, .69, .67)), vein * .5);
|
|
827
|
+
alb *= .92 + (fleck - .5) * .18;
|
|
828
|
+
alb = mix(alb, toLinear(tcol), talpha * .6);
|
|
829
|
+
|
|
830
|
+
float ndv = max(dot(Ns, V), 1e-3);
|
|
831
|
+
float ndl = max(dot(Ns, Ldir), 0.);
|
|
832
|
+
vec3 H = normalize(Ldir + V);
|
|
833
|
+
float aa = .72 * .72; // stone is rough
|
|
834
|
+
vec3 F = F_Schlick(vec3(.035), max(dot(H, V), 0.));
|
|
835
|
+
vec3 st = alb * (.20 + .95 * ndl);
|
|
836
|
+
st += F * D_GGX(max(dot(Ns, H), 0.), aa) * V_SmithGGX(ndv, ndl, aa) * ndl * 3.2 * uSpec;
|
|
837
|
+
st += alb * pow(1. - ndv, 4.) * .25; // dusty edge
|
|
838
|
+
plasma = toSrgb(tonemap(st));
|
|
839
|
+
} else {
|
|
840
|
+
// \u2500\u2500 cloud \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
841
|
+
// The one volume. The SDF is the boundary, density is noise inside it,
|
|
842
|
+
// and the light is marched rather than dotted: transmittance by
|
|
843
|
+
// Beer-Lambert, scattering by Henyey-Greenstein. A lambert cloud looks
|
|
844
|
+
// painted; a marched one looks lit from a direction.
|
|
845
|
+
// Bounded tight to the silhouette, which the edge displacement has
|
|
846
|
+
// already made billowy \u2014 the panel's outline is the cloud's outline. The
|
|
847
|
+
// interior is dense, so it reads as a thing made of cloud rather than as
|
|
848
|
+
// haze lying over a rectangle.
|
|
849
|
+
float inside = smoothstep(-1., 9., sd);
|
|
850
|
+
vec2 dq = warp(p * .0058 + vec2(uTime * .010, uTime * .004), 1.15);
|
|
851
|
+
float base = fbm(dq) * .58 + fbm(dq * 2.7) * .28 + fbm(dq * 6.9) * .14;
|
|
852
|
+
float dens = clamp(inside * (.55 + base * 1.15) - .12, 0., 1.);
|
|
853
|
+
|
|
854
|
+
// March toward the light and accumulate what it has to pass through.
|
|
855
|
+
float shadow = 0.;
|
|
856
|
+
vec2 step2 = normalize(Ldir.xy + vec2(1e-4)) * 16.;
|
|
857
|
+
for (int i = 1; i <= 5; i++) {
|
|
858
|
+
vec2 sp3 = p + step2 * float(i);
|
|
859
|
+
float ins = smoothstep(-1., 9., sd + float(i) * 5.);
|
|
860
|
+
vec2 q3 = warp(sp3 * .0058 + vec2(uTime * .010, uTime * .004), 1.15);
|
|
861
|
+
shadow += clamp(ins * (.55 + fbm(q3) * 1.15) - .12, 0., 1.);
|
|
862
|
+
}
|
|
863
|
+
float transmit = exp(-shadow * .55);
|
|
864
|
+
float cosT = dot(normalize(vec3(n, .6)), Ldir);
|
|
865
|
+
float g = .45;
|
|
866
|
+
float hg = (1. - g*g) / pow(1. + g*g - 2.*g*cosT, 1.5) * .0796;
|
|
867
|
+
vec3 sunCol = toLinear(vec3(1., .96, .89));
|
|
868
|
+
vec3 skyCol = toLinear(vec3(.42, .50, .64));
|
|
869
|
+
vec3 cl = skyCol * (.35 + .65 * dens) + sunCol * transmit * (.55 + 5.5 * hg);
|
|
870
|
+
cl = mix(cl, toLinear(tcol), talpha * .45);
|
|
871
|
+
plasma = toSrgb(tonemap(cl));
|
|
872
|
+
a = smoothstep(.04, .42, dens); // density is the coverage, not the outline
|
|
873
|
+
}
|
|
874
|
+
|
|
293
875
|
col = mix(col, plasma, a);
|
|
294
876
|
grain = 1. - a; // grain is background-only; panels stay clean
|
|
877
|
+
cover = a;
|
|
878
|
+
}
|
|
879
|
+
if (uClear > .5) {
|
|
880
|
+
// Outside a surface only its shadow lands on the page: black at the
|
|
881
|
+
// shadow's darkness. Inside, the surface itself. The edge blends the two
|
|
882
|
+
// by coverage, un-premultiplied for a non-premultiplied canvas.
|
|
883
|
+
float shade = clamp(shStr * smoothstep(.02, .55, hs) * (1. - uLight*.6), 0., 1.);
|
|
884
|
+
float alpha = max(cover, shade);
|
|
885
|
+
o = vec4(col * (cover / max(alpha, 1e-4)), alpha);
|
|
886
|
+
return;
|
|
295
887
|
}
|
|
296
888
|
// Keyed on viewport position, so a frame drawn for a taller region has the
|
|
297
889
|
// same grain in its viewport band as the live frame that replaces it.
|
|
@@ -317,6 +909,14 @@ function hexToRgb(hex) {
|
|
|
317
909
|
}
|
|
318
910
|
|
|
319
911
|
// src/renderer.ts
|
|
912
|
+
var MATERIALS = ["plasma", "crystal", "metal", "mercury", "wood", "stone", "cloud"];
|
|
913
|
+
var FORMING_ATTR = "data-plasma-forming";
|
|
914
|
+
var FORMING_EVENT = "plasmaforming";
|
|
915
|
+
var FORMED_EVENT = "plasmaformed";
|
|
916
|
+
function dispatch(el, name, id) {
|
|
917
|
+
if (typeof CustomEvent === "undefined" || typeof el.dispatchEvent !== "function") return;
|
|
918
|
+
el.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: { id } }));
|
|
919
|
+
}
|
|
320
920
|
var UNIFORMS = [
|
|
321
921
|
"uRes",
|
|
322
922
|
"uView",
|
|
@@ -370,7 +970,17 @@ var UNIFORMS = [
|
|
|
370
970
|
"uShim",
|
|
371
971
|
"uGlow",
|
|
372
972
|
"uWash",
|
|
373
|
-
"uGrain"
|
|
973
|
+
"uGrain",
|
|
974
|
+
"uClear",
|
|
975
|
+
"uMat",
|
|
976
|
+
"uLightDir",
|
|
977
|
+
"uRough",
|
|
978
|
+
"uAniso",
|
|
979
|
+
"uEdge",
|
|
980
|
+
"uEdgeScale",
|
|
981
|
+
"uEdgeSharp",
|
|
982
|
+
"uThick",
|
|
983
|
+
"uTension"
|
|
374
984
|
];
|
|
375
985
|
var MASK_SCALE = 0.5;
|
|
376
986
|
var MAX_PIXELS = 26e5;
|
|
@@ -629,6 +1239,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
629
1239
|
r.formV += (170 * (1 - r.form) - 26 * r.formV) * dt;
|
|
630
1240
|
r.form += r.formV * dt;
|
|
631
1241
|
} else r.form = 1;
|
|
1242
|
+
if (r.forming && r.form > 0.985) {
|
|
1243
|
+
r.forming = false;
|
|
1244
|
+
r.el.removeAttribute(FORMING_ATTR);
|
|
1245
|
+
dispatch(r.el, FORMED_EVENT, r.id);
|
|
1246
|
+
}
|
|
632
1247
|
const b = elementBox(r.el);
|
|
633
1248
|
r.box = b;
|
|
634
1249
|
if (b.w === 0 || b.l > right || b.t > bottom || b.l + b.w < left || b.t + b.h < top) {
|
|
@@ -755,7 +1370,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
755
1370
|
}
|
|
756
1371
|
/** Returns null when WebGL2 is unavailable. */
|
|
757
1372
|
static create(canvas, settings) {
|
|
758
|
-
const gl = canvas.getContext("webgl2", {
|
|
1373
|
+
const gl = canvas.getContext("webgl2", {
|
|
1374
|
+
antialias: false,
|
|
1375
|
+
premultipliedAlpha: false,
|
|
1376
|
+
preserveDrawingBuffer: !!settings.preserveDrawingBuffer
|
|
1377
|
+
});
|
|
759
1378
|
if (!gl) return null;
|
|
760
1379
|
try {
|
|
761
1380
|
return new _PlasmaRenderer(canvas, gl, settings);
|
|
@@ -860,6 +1479,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
860
1479
|
form: this.settings.reducedMotion ? 1 : 0,
|
|
861
1480
|
formV: 0,
|
|
862
1481
|
removing: false,
|
|
1482
|
+
forming: false,
|
|
863
1483
|
lx: 0,
|
|
864
1484
|
ly: 0,
|
|
865
1485
|
leanCss: "",
|
|
@@ -877,6 +1497,11 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
877
1497
|
drawn: null,
|
|
878
1498
|
elevNow: -1
|
|
879
1499
|
};
|
|
1500
|
+
if (!this.settings.reducedMotion && typeof el.setAttribute === "function") {
|
|
1501
|
+
rec.forming = true;
|
|
1502
|
+
el.setAttribute(FORMING_ATTR, "");
|
|
1503
|
+
dispatch(el, FORMING_EVENT, id);
|
|
1504
|
+
} else dispatch(el, FORMED_EVENT, id);
|
|
880
1505
|
this.recs.set(id, rec);
|
|
881
1506
|
return {
|
|
882
1507
|
id,
|
|
@@ -892,6 +1517,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
892
1517
|
remove: () => {
|
|
893
1518
|
el.style.translate = "";
|
|
894
1519
|
el.style.scale = "";
|
|
1520
|
+
if (rec.forming) el.removeAttribute(FORMING_ATTR);
|
|
895
1521
|
this.recs.delete(id);
|
|
896
1522
|
}
|
|
897
1523
|
};
|
|
@@ -1092,6 +1718,8 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
1092
1718
|
gl.uniform3fv(u.uA, this.colors[0]);
|
|
1093
1719
|
gl.uniform3fv(u.uB, this.colors[1]);
|
|
1094
1720
|
gl.uniform3fv(u.uC, this.colors[2]);
|
|
1721
|
+
gl.uniform1f(u.uTension, s.tension);
|
|
1722
|
+
gl.uniform1f(u.uThick, s.thickness);
|
|
1095
1723
|
};
|
|
1096
1724
|
const bl = this.progs.blur;
|
|
1097
1725
|
const pass = (src, dst, dx, dy) => {
|
|
@@ -1127,6 +1755,7 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
1127
1755
|
const bc = this.bgColor;
|
|
1128
1756
|
gl.uniform3f(this.progs.bg.u.uBgColor, bc?.[0] ?? 0, bc?.[1] ?? 0, bc?.[2] ?? 0);
|
|
1129
1757
|
gl.uniform1f(this.progs.bg.u.uBgSolid, bc ? 1 : 0);
|
|
1758
|
+
gl.uniform1f(this.progs.bg.u.uClear, s.ground === "clear" ? 1 : 0);
|
|
1130
1759
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1131
1760
|
gl.activeTexture(gl.TEXTURE0);
|
|
1132
1761
|
gl.viewport(0, 0, this.rtA.w, this.rtA.h);
|
|
@@ -1152,7 +1781,8 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
1152
1781
|
if (this.T[i * 4 + 3] > 2e-3) hasTint = true;
|
|
1153
1782
|
if (this.EL[i] > 2e-3) hasElev = true;
|
|
1154
1783
|
}
|
|
1155
|
-
|
|
1784
|
+
const reflective = s.material === "metal" || s.material === "crystal";
|
|
1785
|
+
if (hasFrost || reflective) {
|
|
1156
1786
|
pass(this.rtBg, this.rtB, 1.5 * k, 0);
|
|
1157
1787
|
pass(this.rtB, this.rtBgM, 0, 2.5 * k);
|
|
1158
1788
|
pass(this.rtBgM, this.rtB, 2.5 * k, 0);
|
|
@@ -1215,6 +1845,16 @@ var _PlasmaRenderer = class _PlasmaRenderer {
|
|
|
1215
1845
|
gl.uniform1f(c.u.uGlow, s.glow);
|
|
1216
1846
|
gl.uniform1f(c.u.uWash, s.wash);
|
|
1217
1847
|
gl.uniform1f(c.u.uGrain, s.grain);
|
|
1848
|
+
gl.uniform1f(c.u.uClear, s.ground === "clear" ? 1 : 0);
|
|
1849
|
+
gl.uniform1f(c.u.uMat, Math.max(0, MATERIALS.indexOf(s.material)));
|
|
1850
|
+
gl.uniform3f(c.u.uLightDir, s.lightDir[0], s.lightDir[1], s.lightDir[2]);
|
|
1851
|
+
gl.uniform1f(c.u.uRough, s.roughness);
|
|
1852
|
+
gl.uniform1f(c.u.uAniso, s.anisotropy);
|
|
1853
|
+
gl.uniform1f(c.u.uEdge, s.edge);
|
|
1854
|
+
gl.uniform1f(c.u.uEdgeScale, s.edgeScale);
|
|
1855
|
+
gl.uniform1f(c.u.uEdgeSharp, s.edgeSharpness);
|
|
1856
|
+
gl.uniform1f(c.u.uThick, s.thickness);
|
|
1857
|
+
gl.uniform1f(c.u.uTension, s.tension);
|
|
1218
1858
|
gl.activeTexture(gl.TEXTURE0);
|
|
1219
1859
|
gl.bindTexture(gl.TEXTURE_2D, this.rtC.tex);
|
|
1220
1860
|
gl.activeTexture(gl.TEXTURE1);
|
|
@@ -1426,6 +2066,17 @@ function PlasmaProvider({
|
|
|
1426
2066
|
wash = 1,
|
|
1427
2067
|
grain = 1,
|
|
1428
2068
|
backgroundBlur = 0,
|
|
2069
|
+
ground = "field",
|
|
2070
|
+
preserveDrawingBuffer = false,
|
|
2071
|
+
material = "plasma",
|
|
2072
|
+
lightDir = [-0.42, -0.62, 0.66],
|
|
2073
|
+
roughness = 0.28,
|
|
2074
|
+
anisotropy = 0,
|
|
2075
|
+
edge = 0,
|
|
2076
|
+
edgeScale = 0.01,
|
|
2077
|
+
edgeSharpness = 0,
|
|
2078
|
+
thickness = 18,
|
|
2079
|
+
tension = 0,
|
|
1429
2080
|
pointerDrop = true,
|
|
1430
2081
|
ambientDrops = false,
|
|
1431
2082
|
grid = 24,
|
|
@@ -1465,6 +2116,17 @@ function PlasmaProvider({
|
|
|
1465
2116
|
wash,
|
|
1466
2117
|
grain,
|
|
1467
2118
|
backgroundBlur,
|
|
2119
|
+
ground,
|
|
2120
|
+
preserveDrawingBuffer,
|
|
2121
|
+
material,
|
|
2122
|
+
lightDir,
|
|
2123
|
+
roughness,
|
|
2124
|
+
anisotropy,
|
|
2125
|
+
edge,
|
|
2126
|
+
edgeScale,
|
|
2127
|
+
edgeSharpness,
|
|
2128
|
+
thickness,
|
|
2129
|
+
tension,
|
|
1468
2130
|
viscosity,
|
|
1469
2131
|
stretch,
|
|
1470
2132
|
flow,
|
|
@@ -1684,6 +2346,8 @@ var PlasmaInner = forwardRef(function Plasma({
|
|
|
1684
2346
|
onDragStart,
|
|
1685
2347
|
onDragEnd,
|
|
1686
2348
|
onJoinChange,
|
|
2349
|
+
onForming,
|
|
2350
|
+
onFormed,
|
|
1687
2351
|
className,
|
|
1688
2352
|
style,
|
|
1689
2353
|
children,
|
|
@@ -1704,6 +2368,8 @@ var PlasmaInner = forwardRef(function Plasma({
|
|
|
1704
2368
|
const [dragging, setDragging] = useState2(false);
|
|
1705
2369
|
const positioned = draggable || !!offset || !!defaultOffset;
|
|
1706
2370
|
const joinCb = useLatest(onJoinChange);
|
|
2371
|
+
const formingCb = useLatest(onForming);
|
|
2372
|
+
const formedCb = useLatest(onFormed);
|
|
1707
2373
|
const sidesStore = useConst(createSidesStore);
|
|
1708
2374
|
const sides = useSyncExternalStore(sidesStore.subscribe, sidesStore.get, noSides);
|
|
1709
2375
|
const runtimeRef = useLatest(runtime);
|
|
@@ -1730,6 +2396,10 @@ var PlasmaInner = forwardRef(function Plasma({
|
|
|
1730
2396
|
useIsoLayoutEffect(() => {
|
|
1731
2397
|
const node = el.current, ren = runtime.renderer;
|
|
1732
2398
|
if (!node || !ren) return;
|
|
2399
|
+
const onForming2 = () => formingCb.current?.();
|
|
2400
|
+
const onFormed2 = () => formedCb.current?.();
|
|
2401
|
+
node.addEventListener(FORMING_EVENT, onForming2);
|
|
2402
|
+
node.addEventListener(FORMED_EVENT, onFormed2);
|
|
1733
2403
|
const h = ren.register(node, optsRef.current, (j) => joinCb.current?.(j), sidesStore.set);
|
|
1734
2404
|
if (positioned) {
|
|
1735
2405
|
h.setLayoutBox(() => {
|
|
@@ -1748,6 +2418,8 @@ var PlasmaInner = forwardRef(function Plasma({
|
|
|
1748
2418
|
return () => {
|
|
1749
2419
|
h.remove();
|
|
1750
2420
|
handle.current = null;
|
|
2421
|
+
node.removeEventListener(FORMING_EVENT, onForming2);
|
|
2422
|
+
node.removeEventListener(FORMED_EVENT, onFormed2);
|
|
1751
2423
|
};
|
|
1752
2424
|
}, [runtime.renderer, positioned]);
|
|
1753
2425
|
useIsoLayoutEffect(() => {
|
|
@@ -1895,6 +2567,10 @@ var PlasmaInner = forwardRef(function Plasma({
|
|
|
1895
2567
|
var Plasma2 = PlasmaInner;
|
|
1896
2568
|
export {
|
|
1897
2569
|
DEFAULT_MAX_SHAPES,
|
|
2570
|
+
FORMED_EVENT,
|
|
2571
|
+
FORMING_ATTR,
|
|
2572
|
+
FORMING_EVENT,
|
|
2573
|
+
MATERIALS,
|
|
1898
2574
|
Plasma2 as Plasma,
|
|
1899
2575
|
PlasmaCanvas,
|
|
1900
2576
|
PlasmaProvider,
|
package/dist/renderer.d.ts
CHANGED
|
@@ -31,6 +31,39 @@ export interface RendererSettings {
|
|
|
31
31
|
grain: number;
|
|
32
32
|
/** Blur applied to the background only, in CSS px (0-40). Costs 8 extra blur passes when above 0. */
|
|
33
33
|
backgroundBlur: number;
|
|
34
|
+
/**
|
|
35
|
+
* What the canvas shows where there is no surface. "field" paints the
|
|
36
|
+
* background everywhere. "clear" leaves it transparent, so the canvas can
|
|
37
|
+
* sit above other content: only the surfaces, their shadows and rims are
|
|
38
|
+
* drawn, and what a surface refracts is the `background` source - usually a
|
|
39
|
+
* live canvas of the field beneath, sampled without swirl so it lines up.
|
|
40
|
+
*/
|
|
41
|
+
ground: "field" | "clear";
|
|
42
|
+
/**
|
|
43
|
+
* Keep each frame in the drawing buffer after it is shown, so another
|
|
44
|
+
* provider can pass this canvas as its `background` and sample it. Off,
|
|
45
|
+
* the browser may clear the buffer after compositing and the sample reads
|
|
46
|
+
* blank. Fixed at creation.
|
|
47
|
+
*/
|
|
48
|
+
preserveDrawingBuffer: boolean;
|
|
49
|
+
/** What the surfaces are made of. */
|
|
50
|
+
material: MaterialName;
|
|
51
|
+
/** Direction the one light comes from. Every opaque material reads it, so they agree. */
|
|
52
|
+
lightDir: [number, number, number];
|
|
53
|
+
/** Surface finish: 0 mirror, 1 chalk. Used by metal; the others carry their own. */
|
|
54
|
+
roughness: number;
|
|
55
|
+
/** How far the highlight stretches along the grain. 0 is isotropic. */
|
|
56
|
+
anisotropy: number;
|
|
57
|
+
/** How far the outline is displaced from the rounded box, in CSS px. 0 leaves it clean. */
|
|
58
|
+
edge: number;
|
|
59
|
+
/** Size of the displacement, in cycles per px. Small is billows, large is chips. */
|
|
60
|
+
edgeScale: number;
|
|
61
|
+
/** 0 rolls the displaced edge, 1 breaks it into flats and points. */
|
|
62
|
+
edgeSharpness: number;
|
|
63
|
+
/** How thick a panel is as a solid, in CSS px. Only the marched materials use it. */
|
|
64
|
+
thickness: number;
|
|
65
|
+
/** Surface tension: how hard the material pulls its own shape toward a bead. */
|
|
66
|
+
tension: number;
|
|
34
67
|
/** 0 = watery and bouncy, 1 = thick and slow. */
|
|
35
68
|
viscosity: number;
|
|
36
69
|
/** How far the surface trails behind moving panels. 0 = no trailing. */
|
|
@@ -50,8 +83,30 @@ export interface RendererSettings {
|
|
|
50
83
|
/** Background: CSS color, image URL, or a live img/canvas/video source (null for the procedural mood field). */
|
|
51
84
|
background: BackgroundSource | null;
|
|
52
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The materials a surface can be made of. `plasma` is the original and the
|
|
88
|
+
* default; the rest share its geometry, its springs and its fusing, and differ
|
|
89
|
+
* only in the composite pass — which is the whole reason they are cheap.
|
|
90
|
+
*/
|
|
91
|
+
export declare const MATERIALS: readonly ["plasma", "crystal", "metal", "mercury", "wood", "stone", "cloud"];
|
|
92
|
+
export type MaterialName = (typeof MATERIALS)[number];
|
|
53
93
|
/** Anything the background can be: a CSS color string, an image URL, or an element to sample (canvas and video update live). */
|
|
54
94
|
export type BackgroundSource = string | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap;
|
|
95
|
+
/**
|
|
96
|
+
* Set on a surface's element from `register` until its form-in has finished
|
|
97
|
+
* (about half a second; never under reduced motion). Style the element's
|
|
98
|
+
* children off it to have the contents arrive after the material:
|
|
99
|
+
* `[data-plasma-forming] > * { opacity: 0 }` with a transition on opacity.
|
|
100
|
+
*/
|
|
101
|
+
export declare const FORMING_ATTR = "data-plasma-forming";
|
|
102
|
+
/**
|
|
103
|
+
* Events a surface's element dispatches (bubbling, so a page can listen at
|
|
104
|
+
* the document): `plasmaforming` when the form-in starts, `plasmaformed`
|
|
105
|
+
* when it has settled — at once under reduced motion. `detail.id` is the
|
|
106
|
+
* surface id. `<Plasma onForming onFormed>` wraps them.
|
|
107
|
+
*/
|
|
108
|
+
export declare const FORMING_EVENT = "plasmaforming";
|
|
109
|
+
export declare const FORMED_EVENT = "plasmaformed";
|
|
55
110
|
export interface ShapeOptions {
|
|
56
111
|
radius: number;
|
|
57
112
|
lean: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxgarden/plasma-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
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": {
|