nodality 1.0.181 → 1.0.184
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/dist/animator.cjs.js +1 -1
- package/dist/animator.esm.js +1 -1
- package/dist/audionew.cjs.js +1 -1
- package/dist/audionew.esm.js +1 -1
- package/dist/base.cjs.js +1 -1
- package/dist/base.esm.js +1 -1
- package/dist/beta-desktop-bar.cjs.js +1 -1
- package/dist/beta-desktop-bar.esm.js +1 -1
- package/dist/beta-mobile-bar.cjs.js +1 -1
- package/dist/beta-mobile-bar.esm.js +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/button.cjs.js +1 -1
- package/dist/button.esm.js +1 -1
- package/dist/center.cjs.js +1 -1
- package/dist/center.esm.js +1 -1
- package/dist/checkbox.cjs.js +1 -1
- package/dist/checkbox.esm.js +1 -1
- package/dist/code.cjs.js +1 -1
- package/dist/code.esm.js +1 -1
- package/dist/container.cjs.js +1 -1
- package/dist/container.esm.js +1 -1
- package/dist/data-list.cjs.js +1 -1
- package/dist/data-list.esm.js +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.esm.js +1 -1
- package/dist/dropdown.cjs.js +1 -1
- package/dist/dropdown.esm.js +1 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/flex-card.cjs.js +1 -1
- package/dist/flex-card.esm.js +1 -1
- package/dist/flex-grid.cjs.js +1 -1
- package/dist/flex-grid.esm.js +1 -1
- package/dist/flex-row.cjs.js +1 -1
- package/dist/flex-row.esm.js +1 -1
- package/dist/floating-input.cjs.js +1 -1
- package/dist/floating-input.esm.js +1 -1
- package/dist/free.cjs.js +1 -1
- package/dist/free.esm.js +1 -1
- package/dist/horizontal-scroller.cjs.js +1 -1
- package/dist/horizontal-scroller.esm.js +1 -1
- package/dist/image-picker.cjs.js +1 -1
- package/dist/image-picker.esm.js +1 -1
- package/dist/image.cjs.js +1 -1
- package/dist/image.esm.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/link-getter.cjs.js +1 -1
- package/dist/link-getter.esm.js +1 -1
- package/dist/link.cjs.js +1 -1
- package/dist/link.esm.js +1 -1
- package/dist/meta-adder.cjs.js +1 -1
- package/dist/meta-adder.esm.js +1 -1
- package/dist/modal-2025.cjs.js +1 -1
- package/dist/modal-2025.esm.js +1 -1
- package/dist/multiswitcher.cjs.js +1 -1
- package/dist/multiswitcher.esm.js +1 -1
- package/dist/new-nav-bar.cjs.js +1 -1
- package/dist/new-nav-bar.esm.js +1 -1
- package/dist/picker.cjs.js +1 -1
- package/dist/picker.esm.js +1 -1
- package/dist/progress.cjs.js +1 -1
- package/dist/progress.esm.js +1 -1
- package/dist/radio.cjs.js +1 -1
- package/dist/radio.esm.js +1 -1
- package/dist/range.cjs.js +1 -1
- package/dist/range.esm.js +1 -1
- package/dist/side-bar.cjs.js +1 -1
- package/dist/side-bar.esm.js +1 -1
- package/dist/side-nav-bar.cjs.js +1 -1
- package/dist/side-nav-bar.esm.js +1 -1
- package/dist/simple-bar.cjs.js +1 -1
- package/dist/simple-bar.esm.js +1 -1
- package/dist/slider-2025.cjs.js +1 -1
- package/dist/slider-2025.esm.js +1 -1
- package/dist/spacer.cjs.js +1 -1
- package/dist/spacer.esm.js +1 -1
- package/dist/stack.cjs.js +1 -1
- package/dist/stack.esm.js +1 -1
- package/dist/table.cjs.js +1 -1
- package/dist/table.esm.js +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.esm.js +1 -1
- package/dist/text.cjs.js +1 -1
- package/dist/text.esm.js +1 -1
- package/dist/ulist.cjs.js +1 -1
- package/dist/ulist.esm.js +1 -1
- package/dist/video.cjs.js +1 -1
- package/dist/video.esm.js +1 -1
- package/dist/wrap.cjs.js +1 -1
- package/dist/wrap.esm.js +1 -1
- package/dist/zoom-card.cjs.js +1 -1
- package/dist/zoom-card.esm.js +1 -1
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base-2.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/box.js +1 -1
- package/layout/button.js +1 -1
- package/layout/cards.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/clean-row.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/custom.js +1 -1
- package/layout/div-image.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/dropdown.js +1 -1
- package/layout/empty-element.js +1 -1
- package/layout/external-stylesheet.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/footer.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-new.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/group.js +1 -1
- package/layout/header.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image-old.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/label.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list-OLD.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/modernwrap.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/multiswitcherBeta.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/navBar-OLD.js +1 -1
- package/layout/new-flat-adder.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/offset-container.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/saved-new-nav-bar.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/styler.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +1 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/without-new.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +1 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/raster-ops.js +662 -230
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/package.json +2 -1
package/lib/raster-ops.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.0.
|
|
2
|
+
* nodality v1.0.184
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -46,6 +46,32 @@
|
|
|
46
46
|
|
|
47
47
|
const MAX_BLOBS = 12;
|
|
48
48
|
|
|
49
|
+
// ── Drivers ──────────────────────────────────────────────────────────
|
|
50
|
+
// What steers a reactive op. Previously "react to the pointer" was
|
|
51
|
+
// hardcoded into offset and duotone; a driver makes the input a value in
|
|
52
|
+
// the node data, so the same op can be aimed at different signals:
|
|
53
|
+
//
|
|
54
|
+
// { op: "offset", by: "mouse" } { op: "offset", by: "scroll" }
|
|
55
|
+
//
|
|
56
|
+
// Each driver returns a focus point in device pixels plus an amount in
|
|
57
|
+
// 0..1 that the op scales its effect by. Ops read them as u<i>_dpos and
|
|
58
|
+
// u<i>_damt; the pipeline evaluates and uploads them once per frame.
|
|
59
|
+
const DRIVERS = {
|
|
60
|
+
mouse: (c) => ({ x: c.mouseX, y: c.mouseY, amt: 1 }),
|
|
61
|
+
// Same focus as mouse, but faded in and out with the hover state, so
|
|
62
|
+
// the effect resolves away when the pointer leaves.
|
|
63
|
+
hover: (c) => ({ x: c.mouseX, y: c.mouseY, amt: c.hover }),
|
|
64
|
+
// A band that travels up the element as it crosses the viewport.
|
|
65
|
+
scroll: (c) => ({ x: c.w * 0.5, y: c.h * c.scroll, amt: 1 }),
|
|
66
|
+
// Hands-free: the focus drifts on a Lissajous path.
|
|
67
|
+
time: (c) => ({
|
|
68
|
+
x: c.w * (0.5 + 0.34 * Math.cos(c.t * 0.6)),
|
|
69
|
+
y: c.h * (0.5 + 0.34 * Math.sin(c.t * 0.8)),
|
|
70
|
+
amt: 1,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
const DRIVER_NAMES = Object.keys(DRIVERS);
|
|
74
|
+
|
|
49
75
|
const REGISTRY = {
|
|
50
76
|
hexalize: {
|
|
51
77
|
stage: "cell",
|
|
@@ -71,13 +97,17 @@ const REGISTRY = {
|
|
|
71
97
|
// version shifted only the texture lookup, which left grid
|
|
72
98
|
// edges frozen in screen space.)
|
|
73
99
|
stage: "warp",
|
|
74
|
-
|
|
100
|
+
// Steered by the pointer unless the node says otherwise, which is
|
|
101
|
+
// what this op did before drivers existed.
|
|
102
|
+
defaultDriver: "mouse",
|
|
103
|
+
decl: (p) => `uniform float ${p}strength; uniform float ${p}radius;
|
|
104
|
+
uniform vec2 ${p}dpos; uniform float ${p}damt;`,
|
|
75
105
|
code: (p) => `
|
|
76
106
|
{
|
|
77
|
-
float d = length(warped -
|
|
107
|
+
float d = length(warped - ${p}dpos);
|
|
78
108
|
float fall = 1.0 - smoothstep(0.0, ${p}radius, d);
|
|
79
|
-
vec2 dir = d > 0.5 ? (warped -
|
|
80
|
-
warped -= dir * ${p}strength * fall;
|
|
109
|
+
vec2 dir = d > 0.5 ? (warped - ${p}dpos) / d : vec2(0.0);
|
|
110
|
+
warped -= dir * ${p}strength * fall * ${p}damt;
|
|
81
111
|
}`,
|
|
82
112
|
uniforms: (node, dpr) => ({
|
|
83
113
|
strength: ["1f", (node.strength || 20) * dpr],
|
|
@@ -87,13 +117,15 @@ const REGISTRY = {
|
|
|
87
117
|
|
|
88
118
|
duotone: {
|
|
89
119
|
stage: "color",
|
|
90
|
-
decl: (p) => `uniform vec3 ${p}a; uniform vec3 ${p}b; uniform float ${p}radius
|
|
120
|
+
decl: (p) => `uniform vec3 ${p}a; uniform vec3 ${p}b; uniform float ${p}radius;
|
|
121
|
+
uniform vec2 ${p}dpos; uniform float ${p}damt;`,
|
|
91
122
|
code: (p, node) => `
|
|
92
123
|
{
|
|
93
124
|
float lum = dot(col, vec3(0.299, 0.587, 0.114));
|
|
94
125
|
vec3 duo = mix(${p}a, ${p}b, lum);
|
|
95
|
-
${node.by
|
|
96
|
-
? `float mask = 1.0 - smoothstep(${p}radius * 0.35, ${p}radius,
|
|
126
|
+
${node.by
|
|
127
|
+
? `float mask = (1.0 - smoothstep(${p}radius * 0.35, ${p}radius,
|
|
128
|
+
length(center - ${p}dpos))) * ${p}damt;`
|
|
97
129
|
: `float mask = 1.0;`}
|
|
98
130
|
col = mix(col, duo, mask);
|
|
99
131
|
}`,
|
|
@@ -146,126 +178,429 @@ const REGISTRY = {
|
|
|
146
178
|
// projection at all — the whole simulation is a single pass.
|
|
147
179
|
//
|
|
148
180
|
// { op: "stir", strength: 26, swirl: 2.4, decay: 0.985 }
|
|
181
|
+
// Halftone: the print dot screen. Tone is carried by dot AREA on a
|
|
182
|
+
// rotated grid rather than by intensity, the way offset lithography
|
|
183
|
+
// reproduces a continuous-tone image with a single ink.
|
|
184
|
+
//
|
|
185
|
+
// { op: "halftone", size: 6, angle: 15, ink: "#0B1B2B" }
|
|
186
|
+
//
|
|
187
|
+
// With a driver the screen coarsens toward the focus, like holding a
|
|
188
|
+
// loupe over the sheet.
|
|
189
|
+
halftone: {
|
|
190
|
+
stage: "color",
|
|
191
|
+
decl: (p) => `
|
|
192
|
+
uniform float ${p}size;
|
|
193
|
+
uniform float ${p}angle;
|
|
194
|
+
uniform vec3 ${p}ink;
|
|
195
|
+
uniform vec3 ${p}paper;
|
|
196
|
+
uniform float ${p}soft;
|
|
197
|
+
uniform float ${p}radius;
|
|
198
|
+
uniform vec2 ${p}dpos;
|
|
199
|
+
uniform float ${p}damt;`,
|
|
200
|
+
code: (p, node) => `
|
|
201
|
+
{
|
|
202
|
+
float lum = dot(col, vec3(0.299, 0.587, 0.114));
|
|
203
|
+
float sz = ${p}size;${node.by ? `
|
|
204
|
+
float fd = 1.0 - smoothstep(0.0, ${p}radius, length(frag - ${p}dpos));
|
|
205
|
+
sz *= 1.0 + fd * ${p}damt * 1.8;` : ``}
|
|
206
|
+
// Screen angle: rotating the grid is what stops the dots
|
|
207
|
+
// reading as a horizontal/vertical texture.
|
|
208
|
+
float ca = cos(${p}angle), sa = sin(${p}angle);
|
|
209
|
+
vec2 rp = vec2(ca * frag.x - sa * frag.y,
|
|
210
|
+
sa * frag.x + ca * frag.y) / max(sz, 1.0);
|
|
211
|
+
vec2 cell = fract(rp) - 0.5;
|
|
212
|
+
// Dot radius from tone: darker tone prints a bigger dot.
|
|
213
|
+
// sqrt because ink coverage goes as area, not radius.
|
|
214
|
+
float rr = sqrt(clamp(1.0 - lum, 0.0, 1.0)) * 0.55;
|
|
215
|
+
float dm = smoothstep(rr, rr - ${p}soft, length(cell));
|
|
216
|
+
col = mix(${p}paper, ${p}ink, dm);
|
|
217
|
+
}`,
|
|
218
|
+
uniforms: (node, dpr) => ({
|
|
219
|
+
size: ["1f", (node.size || 6) * dpr],
|
|
220
|
+
angle: ["1f", ((node.angle != null ? node.angle : 15) * Math.PI) / 180],
|
|
221
|
+
ink: ["3fv", hexToRgb(node.ink || "#0B1B2B")],
|
|
222
|
+
paper: ["3fv", hexToRgb(node.paper || "#FFFFFF")],
|
|
223
|
+
soft: ["1f", node.softness != null ? node.softness : 0.08],
|
|
224
|
+
radius: ["1f", (node.radius || 220) * dpr],
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// Chromatic aberration: the only op that uses the displace stage. It
|
|
229
|
+
// does not move the sample position itself, it sets `chroma`, the
|
|
230
|
+
// per-channel split the sampler applies - red and blue are fetched
|
|
231
|
+
// either side of green along the radial direction, the way a simple
|
|
232
|
+
// lens disperses wavelengths toward its edges.
|
|
233
|
+
//
|
|
234
|
+
// { op: "aberration", amount: 6 } // radial, lens-like
|
|
235
|
+
// { op: "aberration", amount: 14, by: "mouse" } // focused at the pointer
|
|
236
|
+
aberration: {
|
|
237
|
+
stage: "displace",
|
|
238
|
+
chroma: true,
|
|
239
|
+
decl: (p) => `
|
|
240
|
+
uniform float ${p}amount;
|
|
241
|
+
uniform float ${p}radius;
|
|
242
|
+
uniform vec2 ${p}dpos;
|
|
243
|
+
uniform float ${p}damt;`,
|
|
244
|
+
code: (p, node) => `
|
|
245
|
+
{
|
|
246
|
+
vec2 ctr = ${node.by ? `${p}dpos` : `u_res * 0.5`};
|
|
247
|
+
vec2 dv = sampleP - ctr;
|
|
248
|
+
float dl = length(dv);
|
|
249
|
+
vec2 dir = dl > 0.5 ? dv / dl : vec2(0.0);
|
|
250
|
+
${node.by
|
|
251
|
+
? `float amt = ${p}amount * ${p}damt *
|
|
252
|
+
(1.0 - smoothstep(0.0, ${p}radius, dl));`
|
|
253
|
+
: `float amt = ${p}amount * (dl / max(length(u_res) * 0.5, 1.0));`}
|
|
254
|
+
chroma += dir * amt;
|
|
255
|
+
}`,
|
|
256
|
+
uniforms: (node, dpr) => ({
|
|
257
|
+
amount: ["1f", (node.amount != null ? node.amount : 6) * dpr],
|
|
258
|
+
radius: ["1f", (node.radius || 240) * dpr],
|
|
259
|
+
}),
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// Stirred liquid: an incompressible fluid whose velocity field warps
|
|
263
|
+
// the content and whose dye field colours it.
|
|
264
|
+
//
|
|
265
|
+
// Implemented from the published algorithm, not from any particular
|
|
266
|
+
// existing codebase:
|
|
267
|
+
// J. Stam, "Stable Fluids", SIGGRAPH '99, 121-128 - the
|
|
268
|
+
// unconditionally stable operator splitting used here: add
|
|
269
|
+
// forces, advect by a backward particle trace, then project the
|
|
270
|
+
// velocity onto its divergence-free part via a Poisson solve.
|
|
271
|
+
// M. Harris, "Fast Fluid Dynamics Simulation on the GPU", GPU Gems
|
|
272
|
+
// ch. 38 (2003) - mapping each operator onto a fragment program
|
|
273
|
+
// over ping-ponged textures, Jacobi iterations for the pressure.
|
|
274
|
+
// R. Fedkiw, J. Stam, H. Jensen, "Visual Simulation of Smoke",
|
|
275
|
+
// SIGGRAPH '01 - vorticity confinement, which restores the
|
|
276
|
+
// small-scale swirl that a coarse grid numerically damps away.
|
|
277
|
+
//
|
|
278
|
+
// Per frame: splat -> curl -> confine -> divergence -> pressure
|
|
279
|
+
// (Jacobi) -> subtract gradient -> advect velocity -> advect dye.
|
|
280
|
+
// The projection step is what distinguishes this from a plain
|
|
281
|
+
// velocity-texture smear: enforcing div(v)=0 is what makes the flow
|
|
282
|
+
// roll into persistent vortices instead of piling up and dissipating.
|
|
283
|
+
//
|
|
284
|
+
// Needs WebGL2 with float render targets; without them the op
|
|
285
|
+
// disables itself and the rest of the chain still renders.
|
|
149
286
|
stir: {
|
|
150
|
-
// Warp: swirl the coordinate space (so a cell op downstream tiles
|
|
151
|
-
// the ALREADY-stirred content). Color: a faint moving sheen.
|
|
152
287
|
stage: ["warp", "color"],
|
|
153
288
|
decl: (p) => `
|
|
154
|
-
uniform sampler2D ${p}
|
|
289
|
+
uniform sampler2D ${p}vel;
|
|
290
|
+
uniform sampler2D ${p}dye;
|
|
155
291
|
uniform float ${p}strength;
|
|
292
|
+
uniform float ${p}dyemix;
|
|
293
|
+
uniform float ${p}intensity;
|
|
156
294
|
uniform float ${p}sheen;
|
|
157
|
-
uniform vec3 ${p}tint
|
|
158
|
-
uniform float ${p}enc;
|
|
159
|
-
uniform float ${p}range;
|
|
160
|
-
vec2 ${p}flow(vec2 uvp) {
|
|
161
|
-
vec4 t = texture2D(${p}field, clamp(uvp, 0.001, 0.999));
|
|
162
|
-
return ${p}enc > 0.5 ? (t.xy * 2.0 - 1.0) * ${p}range : t.xy;
|
|
163
|
-
}`,
|
|
295
|
+
uniform vec3 ${p}tint;`,
|
|
164
296
|
code: (p, node, stage) => stage === "warp" ? `
|
|
165
297
|
{
|
|
166
|
-
// frag/warped are top-down pixels
|
|
167
|
-
// [0,1]
|
|
168
|
-
//
|
|
298
|
+
// frag/warped are top-down pixels, the field is bottom-up
|
|
299
|
+
// [0,1]: flip y going in, and flip the y of the vector coming
|
|
300
|
+
// out so the content drags WITH the pointer.
|
|
169
301
|
vec2 fuv = vec2(warped.x / u_res.x, 1.0 - warped.y / u_res.y);
|
|
170
|
-
vec2
|
|
171
|
-
warped += vec2(
|
|
302
|
+
vec2 v = texture2D(${p}vel, clamp(fuv, 0.001, 0.999)).xy;
|
|
303
|
+
warped += vec2(v.x, -v.y) * ${p}strength;
|
|
172
304
|
}` : `
|
|
173
305
|
{
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
306
|
+
// Sampled at the CONTINUOUS warped position, never at the
|
|
307
|
+
// cell centre: a cell op upstream (hexalize) snaps the cell
|
|
308
|
+
// centre to one point per tile, which would quantise the
|
|
309
|
+
// liquid into flat per-hex patches instead of smooth swirls.
|
|
310
|
+
vec2 suv = vec2(warped.x / u_res.x, 1.0 - warped.y / u_res.y);
|
|
311
|
+
vec3 dc = texture2D(${p}dye, clamp(suv, 0.001, 0.999)).rgb;
|
|
312
|
+
float mag = length(dc);
|
|
313
|
+
// Colour is carried IN the dye field and advected with it, so
|
|
314
|
+
// a ribbon keeps its hue while the flow folds it. Deriving
|
|
315
|
+
// colour from the local flow direction instead would make
|
|
316
|
+
// neighbouring pixels jump hue and read as confetti.
|
|
317
|
+
vec3 liquid = mag > 1e-4 ? clamp(dc / mag * 1.25, 0.0, 1.0) : ${p}tint;
|
|
318
|
+
float amt = clamp((1.0 - exp(-mag * ${p}intensity)) * ${p}dyemix, 0.0, 1.0);
|
|
319
|
+
// Straight-alpha OVER so a later overlay op (blobs) still
|
|
320
|
+
// composites correctly on top of the liquid.
|
|
321
|
+
float na = amt + ovA * (1.0 - amt);
|
|
322
|
+
ovCol = (liquid * amt + ovCol * ovA * (1.0 - amt)) / max(na, 1e-4);
|
|
323
|
+
ovA = na;
|
|
324
|
+
|
|
325
|
+
// Faint sheen on the content itself where the flow is fast.
|
|
326
|
+
float sp = length(texture2D(${p}vel, clamp(suv, 0.001, 0.999)).xy);
|
|
327
|
+
col = mix(col, ${p}tint, (1.0 - exp(-sp * 0.7)) * ${p}sheen);
|
|
180
328
|
}`,
|
|
181
329
|
uniforms: (node, dpr) => ({
|
|
182
330
|
strength: ["1f", (node.strength != null ? node.strength : 26) * dpr],
|
|
183
|
-
sheen: ["1f", node.sheen != null ? node.sheen : 0.
|
|
184
|
-
tint: ["3fv", hexToRgb(node.tint || "#
|
|
331
|
+
sheen: ["1f", node.sheen != null ? node.sheen : 0.15],
|
|
332
|
+
tint: ["3fv", hexToRgb(node.tint || "#7FD4FF")],
|
|
333
|
+
dyemix: ["1f", node.dye != null ? node.dye : 0.85],
|
|
334
|
+
intensity: ["1f", node.intensity != null ? node.intensity : 1.6],
|
|
185
335
|
}),
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
336
|
+
|
|
337
|
+
// ── The simulation ────────────────────────────────────────────
|
|
338
|
+
// Declared as data: the pipeline allocates the targets, compiles
|
|
339
|
+
// the programs, and calls step() once per frame.
|
|
340
|
+
solver: {
|
|
341
|
+
resolutions: (node) => ({
|
|
342
|
+
sim: Math.max(32, Math.min(node.resolution || 128, 512)),
|
|
343
|
+
dye: Math.max(64, Math.min(node.dyeResolution || 512, 1024)),
|
|
344
|
+
}),
|
|
345
|
+
// read/write pairs ping-pong; single targets are scratch.
|
|
346
|
+
targets: {
|
|
347
|
+
velocity: { double: true, res: "sim", fmt: "rg", smooth: true },
|
|
348
|
+
dye: { double: true, res: "dye", fmt: "rgba", smooth: true },
|
|
349
|
+
pressure: { double: true, res: "sim", fmt: "r", smooth: false },
|
|
350
|
+
divergence: { double: false, res: "sim", fmt: "r", smooth: false },
|
|
351
|
+
vorticity: { double: false, res: "sim", fmt: "r", smooth: false },
|
|
352
|
+
},
|
|
353
|
+
// Which target each main-pass sampler reads.
|
|
354
|
+
samplers: { vel: "velocity", dye: "dye" },
|
|
355
|
+
|
|
356
|
+
programs: {
|
|
357
|
+
// Gaussian source term. Distance is measured as a
|
|
358
|
+
// fraction of the element's LONGER side so the radius
|
|
359
|
+
// means the same thing whatever the element's shape.
|
|
360
|
+
splat: `
|
|
191
361
|
precision highp float;
|
|
192
|
-
uniform sampler2D
|
|
193
|
-
uniform vec2
|
|
194
|
-
uniform
|
|
195
|
-
uniform
|
|
196
|
-
uniform float
|
|
197
|
-
uniform float
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
uniform float s_range;
|
|
206
|
-
|
|
207
|
-
vec2 rd(vec2 uvp) {
|
|
208
|
-
vec4 t = texture2D(s_field, clamp(uvp, 0.0, 1.0));
|
|
209
|
-
return s_enc > 0.5 ? (t.xy * 2.0 - 1.0) * s_range : t.xy;
|
|
210
|
-
}
|
|
362
|
+
uniform sampler2D u_src;
|
|
363
|
+
uniform vec2 u_texel;
|
|
364
|
+
uniform vec2 u_point;
|
|
365
|
+
uniform vec3 u_amount;
|
|
366
|
+
uniform float u_radius;
|
|
367
|
+
uniform float u_aspect;
|
|
368
|
+
void main() {
|
|
369
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
370
|
+
vec2 d = uv - u_point;
|
|
371
|
+
if (u_aspect >= 1.0) d.y /= u_aspect; else d.x *= u_aspect;
|
|
372
|
+
float g = exp(-dot(d, d) / max(u_radius * u_radius, 1e-6));
|
|
373
|
+
gl_FragColor = vec4(texture2D(u_src, uv).xyz + u_amount * g, 1.0);
|
|
374
|
+
}`,
|
|
211
375
|
|
|
376
|
+
// Semi-Lagrangian advection (Stam 1999): trace the
|
|
377
|
+
// characteristic backwards and resample. Unconditionally
|
|
378
|
+
// stable for any timestep, which is the whole point.
|
|
379
|
+
advect: `
|
|
380
|
+
precision highp float;
|
|
381
|
+
uniform sampler2D u_vel;
|
|
382
|
+
uniform sampler2D u_src;
|
|
383
|
+
uniform vec2 u_texel;
|
|
384
|
+
uniform float u_dt;
|
|
385
|
+
uniform float u_diss;
|
|
212
386
|
void main() {
|
|
213
|
-
vec2 uv = gl_FragCoord.xy *
|
|
214
|
-
vec2
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
float
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
float cs = cos(th), sn = sin(th);
|
|
232
|
-
a = mat2(cs, sn, -sn, cs) * a;
|
|
233
|
-
|
|
234
|
-
// 4. Viscous decay (frame-rate independent).
|
|
235
|
-
a *= pow(s_decay, s_dt * 60.0);
|
|
236
|
-
|
|
237
|
-
// 5. The spoon: a gaussian impulse of pointer velocity.
|
|
238
|
-
vec2 d = uv - s_point;
|
|
239
|
-
d.x *= s_aspect;
|
|
240
|
-
a += s_impulse * exp(-dot(d, d) / max(s_radius * s_radius, 1e-6));
|
|
241
|
-
|
|
242
|
-
// 6. Optional hands-free stir: a source orbiting the centre.
|
|
243
|
-
if (s_idle > 0.0) {
|
|
244
|
-
float ia = s_time * 0.6;
|
|
245
|
-
vec2 ic = vec2(0.5 + 0.22 * cos(ia), 0.5 + 0.22 * sin(ia));
|
|
246
|
-
vec2 id = uv - ic;
|
|
247
|
-
id.x *= s_aspect;
|
|
248
|
-
a += vec2(-sin(ia), cos(ia)) * s_idle *
|
|
249
|
-
exp(-dot(id, id) / max(s_radius * s_radius, 1e-6)) * s_dt * 60.0;
|
|
250
|
-
}
|
|
387
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
388
|
+
vec2 back = uv - texture2D(u_vel, uv).xy * u_dt;
|
|
389
|
+
gl_FragColor = vec4(texture2D(u_src, clamp(back, 0.0, 1.0)).xyz * u_diss, 1.0);
|
|
390
|
+
}`,
|
|
391
|
+
|
|
392
|
+
// Scalar vorticity of a 2D field: d(vy)/dx - d(vx)/dy.
|
|
393
|
+
curl: `
|
|
394
|
+
precision highp float;
|
|
395
|
+
uniform sampler2D u_vel;
|
|
396
|
+
uniform vec2 u_texel;
|
|
397
|
+
void main() {
|
|
398
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
399
|
+
float r = texture2D(u_vel, uv + vec2(u_texel.x, 0.0)).y;
|
|
400
|
+
float l = texture2D(u_vel, uv - vec2(u_texel.x, 0.0)).y;
|
|
401
|
+
float t = texture2D(u_vel, uv + vec2(0.0, u_texel.y)).x;
|
|
402
|
+
float b = texture2D(u_vel, uv - vec2(0.0, u_texel.y)).x;
|
|
403
|
+
gl_FragColor = vec4(0.5 * ((r - l) - (t - b)), 0.0, 0.0, 1.0);
|
|
404
|
+
}`,
|
|
251
405
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
406
|
+
// Vorticity confinement (Fedkiw et al. 2001): push the
|
|
407
|
+
// velocity along N x omega, where N is the unit gradient
|
|
408
|
+
// of |omega|, i.e. toward the centre of each eddy. This
|
|
409
|
+
// feeds energy back into swirl the grid would otherwise
|
|
410
|
+
// smooth out.
|
|
411
|
+
confine: `
|
|
412
|
+
precision highp float;
|
|
413
|
+
uniform sampler2D u_vel;
|
|
414
|
+
uniform sampler2D u_curl;
|
|
415
|
+
uniform vec2 u_texel;
|
|
416
|
+
uniform float u_eps;
|
|
417
|
+
uniform float u_dt;
|
|
418
|
+
void main() {
|
|
419
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
420
|
+
float r = texture2D(u_curl, uv + vec2(u_texel.x, 0.0)).x;
|
|
421
|
+
float l = texture2D(u_curl, uv - vec2(u_texel.x, 0.0)).x;
|
|
422
|
+
float t = texture2D(u_curl, uv + vec2(0.0, u_texel.y)).x;
|
|
423
|
+
float b = texture2D(u_curl, uv - vec2(0.0, u_texel.y)).x;
|
|
424
|
+
float c = texture2D(u_curl, uv).x;
|
|
425
|
+
vec2 grad = 0.5 * vec2(abs(r) - abs(l), abs(t) - abs(b));
|
|
426
|
+
vec2 n = grad / (length(grad) + 1e-5);
|
|
427
|
+
vec2 force = u_eps * c * vec2(n.y, -n.x);
|
|
428
|
+
vec2 v = texture2D(u_vel, uv).xy + force * u_dt;
|
|
429
|
+
gl_FragColor = vec4(clamp(v, -64.0, 64.0), 0.0, 1.0);
|
|
430
|
+
}`,
|
|
255
431
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
432
|
+
// div(v). The walls reflect, so the fluid stays in the box.
|
|
433
|
+
divergence: `
|
|
434
|
+
precision highp float;
|
|
435
|
+
uniform sampler2D u_vel;
|
|
436
|
+
uniform vec2 u_texel;
|
|
437
|
+
void main() {
|
|
438
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
439
|
+
float r = texture2D(u_vel, uv + vec2(u_texel.x, 0.0)).x;
|
|
440
|
+
float l = texture2D(u_vel, uv - vec2(u_texel.x, 0.0)).x;
|
|
441
|
+
float t = texture2D(u_vel, uv + vec2(0.0, u_texel.y)).y;
|
|
442
|
+
float b = texture2D(u_vel, uv - vec2(0.0, u_texel.y)).y;
|
|
443
|
+
vec2 c = texture2D(u_vel, uv).xy;
|
|
444
|
+
if (uv.x - u_texel.x < 0.0) l = -c.x;
|
|
445
|
+
if (uv.x + u_texel.x > 1.0) r = -c.x;
|
|
446
|
+
if (uv.y - u_texel.y < 0.0) b = -c.y;
|
|
447
|
+
if (uv.y + u_texel.y > 1.0) t = -c.y;
|
|
448
|
+
gl_FragColor = vec4(0.5 * ((r - l) + (t - b)), 0.0, 0.0, 1.0);
|
|
259
449
|
}`,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
450
|
+
|
|
451
|
+
// One Jacobi sweep of the Poisson equation lap(p) = div(v)
|
|
452
|
+
// (Harris 2003). Run a handful of these per frame.
|
|
453
|
+
jacobi: `
|
|
454
|
+
precision highp float;
|
|
455
|
+
uniform sampler2D u_p;
|
|
456
|
+
uniform sampler2D u_div;
|
|
457
|
+
uniform vec2 u_texel;
|
|
458
|
+
void main() {
|
|
459
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
460
|
+
float r = texture2D(u_p, uv + vec2(u_texel.x, 0.0)).x;
|
|
461
|
+
float l = texture2D(u_p, uv - vec2(u_texel.x, 0.0)).x;
|
|
462
|
+
float t = texture2D(u_p, uv + vec2(0.0, u_texel.y)).x;
|
|
463
|
+
float b = texture2D(u_p, uv - vec2(0.0, u_texel.y)).x;
|
|
464
|
+
float d = texture2D(u_div, uv).x;
|
|
465
|
+
gl_FragColor = vec4((r + l + t + b - d) * 0.25, 0.0, 0.0, 1.0);
|
|
466
|
+
}`,
|
|
467
|
+
|
|
468
|
+
// v <- v - grad(p): the projection itself. After this the
|
|
469
|
+
// velocity is (discretely) divergence free.
|
|
470
|
+
subtract: `
|
|
471
|
+
precision highp float;
|
|
472
|
+
uniform sampler2D u_p;
|
|
473
|
+
uniform sampler2D u_vel;
|
|
474
|
+
uniform vec2 u_texel;
|
|
475
|
+
void main() {
|
|
476
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
477
|
+
float r = texture2D(u_p, uv + vec2(u_texel.x, 0.0)).x;
|
|
478
|
+
float l = texture2D(u_p, uv - vec2(u_texel.x, 0.0)).x;
|
|
479
|
+
float t = texture2D(u_p, uv + vec2(0.0, u_texel.y)).x;
|
|
480
|
+
float b = texture2D(u_p, uv - vec2(0.0, u_texel.y)).x;
|
|
481
|
+
vec2 v = texture2D(u_vel, uv).xy - 0.5 * vec2(r - l, t - b);
|
|
482
|
+
gl_FragColor = vec4(v, 0.0, 1.0);
|
|
483
|
+
}`,
|
|
484
|
+
|
|
485
|
+
// Scale a target in place (used to carry a fraction of
|
|
486
|
+
// the previous pressure into the next solve).
|
|
487
|
+
decay: `
|
|
488
|
+
precision highp float;
|
|
489
|
+
uniform sampler2D u_tex;
|
|
490
|
+
uniform vec2 u_texel;
|
|
491
|
+
uniform float u_value;
|
|
492
|
+
void main() {
|
|
493
|
+
gl_FragColor = texture2D(u_tex, gl_FragCoord.xy * u_texel) * u_value;
|
|
494
|
+
}`,
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
// Colour dropped in by the spoon. With rainbow on, the hue
|
|
498
|
+
// comes from the stroke direction at the moment of injection
|
|
499
|
+
// and is then carried by the dye, so it stays coherent as the
|
|
500
|
+
// flow folds it.
|
|
501
|
+
splatColor: (node, dx, dy) => {
|
|
502
|
+
const amt = node.dyeAmount != null ? node.dyeAmount : 0.6;
|
|
503
|
+
if (!node.rainbow) {
|
|
504
|
+
const c = hexToRgb(node.tint || "#7FD4FF");
|
|
505
|
+
return [c[0] * amt, c[1] * amt, c[2] * amt];
|
|
506
|
+
}
|
|
507
|
+
const a = Math.atan2(dy, dx);
|
|
508
|
+
return [
|
|
509
|
+
(0.5 + 0.5 * Math.cos(a)) * amt,
|
|
510
|
+
(0.5 + 0.5 * Math.cos(a + 2.0944)) * amt,
|
|
511
|
+
(0.5 + 0.5 * Math.cos(a + 4.1888)) * amt,
|
|
512
|
+
];
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
step: (S, node, ctx) => {
|
|
516
|
+
const dt = Math.min(ctx.dt, 1 / 30);
|
|
517
|
+
const simT = 1 / S.res.sim;
|
|
518
|
+
const dyeT = 1 / S.res.dye;
|
|
519
|
+
const pow = (base, d) => Math.pow(base, d * 60);
|
|
520
|
+
|
|
521
|
+
// 1. Forces: splat momentum and dye at the pointer.
|
|
522
|
+
if (ctx.moving) {
|
|
523
|
+
const f = node.force != null ? node.force : 1.0;
|
|
524
|
+
S.use("splat");
|
|
525
|
+
S.f1("u_radius", node.radius != null ? node.radius : 0.1);
|
|
526
|
+
S.f1("u_aspect", ctx.aspect);
|
|
527
|
+
S.f2("u_point", ctx.px, ctx.py);
|
|
528
|
+
|
|
529
|
+
S.f2("u_texel", simT, simT);
|
|
530
|
+
S.smp("u_src", S.read("velocity"), 0);
|
|
531
|
+
S.f3("u_amount", ctx.dx * f, ctx.dy * f, 0);
|
|
532
|
+
S.blit(S.write("velocity"));
|
|
533
|
+
S.swap("velocity");
|
|
534
|
+
|
|
535
|
+
S.f2("u_texel", dyeT, dyeT);
|
|
536
|
+
S.smp("u_src", S.read("dye"), 0);
|
|
537
|
+
S.f3("u_amount", ctx.col[0], ctx.col[1], ctx.col[2]);
|
|
538
|
+
S.blit(S.write("dye"));
|
|
539
|
+
S.swap("dye");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 2. Vorticity confinement.
|
|
543
|
+
S.use("curl");
|
|
544
|
+
S.f2("u_texel", simT, simT);
|
|
545
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
546
|
+
S.blit(S.single("vorticity"));
|
|
547
|
+
|
|
548
|
+
S.use("confine");
|
|
549
|
+
S.f2("u_texel", simT, simT);
|
|
550
|
+
S.f1("u_eps", node.curl != null ? node.curl : 2.2);
|
|
551
|
+
S.f1("u_dt", dt);
|
|
552
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
553
|
+
S.smp("u_curl", S.single("vorticity"), 1);
|
|
554
|
+
S.blit(S.write("velocity"));
|
|
555
|
+
S.swap("velocity");
|
|
556
|
+
|
|
557
|
+
// 3. Projection: divergence -> pressure -> subtract.
|
|
558
|
+
S.use("divergence");
|
|
559
|
+
S.f2("u_texel", simT, simT);
|
|
560
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
561
|
+
S.blit(S.single("divergence"));
|
|
562
|
+
|
|
563
|
+
S.use("decay");
|
|
564
|
+
S.f2("u_texel", simT, simT);
|
|
565
|
+
S.f1("u_value", pow(node.pressure != null ? node.pressure : 0.8, dt));
|
|
566
|
+
S.smp("u_tex", S.read("pressure"), 0);
|
|
567
|
+
S.blit(S.write("pressure"));
|
|
568
|
+
S.swap("pressure");
|
|
569
|
+
|
|
570
|
+
S.use("jacobi");
|
|
571
|
+
S.f2("u_texel", simT, simT);
|
|
572
|
+
S.smp("u_div", S.single("divergence"), 1);
|
|
573
|
+
const iters = Math.max(1, Math.min(node.pressureIterations || 6, 40));
|
|
574
|
+
for (let k = 0; k < iters; k++) {
|
|
575
|
+
S.smp("u_p", S.read("pressure"), 0);
|
|
576
|
+
S.blit(S.write("pressure"));
|
|
577
|
+
S.swap("pressure");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
S.use("subtract");
|
|
581
|
+
S.f2("u_texel", simT, simT);
|
|
582
|
+
S.smp("u_p", S.read("pressure"), 0);
|
|
583
|
+
S.smp("u_vel", S.read("velocity"), 1);
|
|
584
|
+
S.blit(S.write("velocity"));
|
|
585
|
+
S.swap("velocity");
|
|
586
|
+
|
|
587
|
+
// 4. Advection: velocity by itself, then dye by velocity.
|
|
588
|
+
S.use("advect");
|
|
589
|
+
S.f1("u_dt", dt);
|
|
590
|
+
S.f2("u_texel", simT, simT);
|
|
591
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
592
|
+
S.smp("u_src", S.read("velocity"), 0);
|
|
593
|
+
S.f1("u_diss", pow(node.decay != null ? node.decay : 0.985, dt));
|
|
594
|
+
S.blit(S.write("velocity"));
|
|
595
|
+
S.swap("velocity");
|
|
596
|
+
|
|
597
|
+
S.f2("u_texel", dyeT, dyeT);
|
|
598
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
599
|
+
S.smp("u_src", S.read("dye"), 1);
|
|
600
|
+
S.f1("u_diss", pow(node.dyeFade != null ? node.dyeFade : 0.97, dt));
|
|
601
|
+
S.blit(S.write("dye"));
|
|
602
|
+
S.swap("dye");
|
|
603
|
+
},
|
|
269
604
|
},
|
|
270
605
|
},
|
|
271
606
|
|
|
@@ -575,6 +910,13 @@ function buildFragmentShader(nodes) {
|
|
|
575
910
|
// darkening only applies when a cell op runs without an edges op.
|
|
576
911
|
const hasEdges = nodes.some((n) => n.op === "edges");
|
|
577
912
|
const defaultSeam = hasEdges ? "" : " col *= 1.0 - edge * 0.55;";
|
|
913
|
+
// Per-channel resampling costs two extra fetches, so it is only
|
|
914
|
+
// emitted when a displace-stage op actually asks for dispersion.
|
|
915
|
+
const hasChroma = nodes.some((n) => (REGISTRY[n.op] || {}).chroma);
|
|
916
|
+
const chromaFetch = hasChroma ? `
|
|
917
|
+
vec2 cpx = vec2(chroma.x, -chroma.y) / u_res;
|
|
918
|
+
col.r = texture2D(u_tex, clamp(uv + cpx, 0.001, 0.999)).r;
|
|
919
|
+
col.b = texture2D(u_tex, clamp(uv - cpx, 0.001, 0.999)).b;` : "";
|
|
578
920
|
return `
|
|
579
921
|
precision highp float;
|
|
580
922
|
uniform sampler2D u_tex;
|
|
@@ -590,10 +932,11 @@ ${warp}
|
|
|
590
932
|
float edge = 0.0;
|
|
591
933
|
${cell}
|
|
592
934
|
vec2 sampleP = warped;
|
|
935
|
+
vec2 chroma = vec2(0.0);
|
|
593
936
|
${displace}
|
|
594
937
|
vec2 uv = vec2(sampleP.x / u_res.x, 1.0 - sampleP.y / u_res.y);
|
|
595
938
|
vec4 tex = texture2D(u_tex, clamp(uv, 0.001, 0.999));
|
|
596
|
-
vec3 col = tex.rgb
|
|
939
|
+
vec3 col = tex.rgb;${chromaFetch}
|
|
597
940
|
vec3 edgeCol = vec3(1.0);
|
|
598
941
|
float edgeCov = 0.0;
|
|
599
942
|
vec3 ovCol = vec3(0.0);
|
|
@@ -795,133 +1138,171 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
795
1138
|
});
|
|
796
1139
|
let lastFrame = (typeof performance !== "undefined") ? performance.now() : 0;
|
|
797
1140
|
|
|
798
|
-
// ──
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
|
|
1141
|
+
// ── Fluid solvers (ops with a `solver` descriptor, e.g. stir) ─────
|
|
1142
|
+
// An op may declare a set of render targets and fragment programs
|
|
1143
|
+
// plus a step() that sequences the passes. The pipeline allocates
|
|
1144
|
+
// everything here, runs step() once per frame, and binds the named
|
|
1145
|
+
// result textures into the main composite pass.
|
|
1146
|
+
//
|
|
1147
|
+
// Float render targets are required (velocity and pressure are
|
|
1148
|
+
// signed and need range well outside [0,1]). Without them the op
|
|
1149
|
+
// disables itself rather than rendering something wrong.
|
|
1150
|
+
const solverOps = [];
|
|
1151
|
+
const makeSolver = (node, i) => {
|
|
806
1152
|
const def = REGISTRY[node.op];
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
845
|
-
return targets;
|
|
1153
|
+
if (!isGL2 || !gl.getExtension("EXT_color_buffer_float")) {
|
|
1154
|
+
console.info("[nodality] '" + node.op +
|
|
1155
|
+
"' needs WebGL2 float render targets - op skipped.");
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
gl.getExtension("OES_texture_float_linear");
|
|
1159
|
+
const res = def.solver.resolutions(node);
|
|
1160
|
+
const FMT = {
|
|
1161
|
+
r: [gl.R16F, gl.RED],
|
|
1162
|
+
rg: [gl.RG16F, gl.RG],
|
|
1163
|
+
rgba: [gl.RGBA16F, gl.RGBA],
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
const made = [];
|
|
1167
|
+
const mkTarget = (size, fmt, smooth) => {
|
|
1168
|
+
const [internal, format] = FMT[fmt];
|
|
1169
|
+
const t = gl.createTexture();
|
|
1170
|
+
gl.bindTexture(gl.TEXTURE_2D, t);
|
|
1171
|
+
const flt = smooth ? gl.LINEAR : gl.NEAREST;
|
|
1172
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, flt);
|
|
1173
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, flt);
|
|
1174
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1175
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1176
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internal, size, size, 0,
|
|
1177
|
+
format, gl.HALF_FLOAT, null);
|
|
1178
|
+
const fbo = gl.createFramebuffer();
|
|
1179
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1180
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
|
|
1181
|
+
gl.TEXTURE_2D, t, 0);
|
|
1182
|
+
const ok = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
1183
|
+
gl.viewport(0, 0, size, size);
|
|
1184
|
+
gl.clearColor(0, 0, 0, 1);
|
|
1185
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1186
|
+
const rec = { tex: t, fbo, size, ok };
|
|
1187
|
+
made.push(rec);
|
|
1188
|
+
return rec;
|
|
846
1189
|
};
|
|
847
1190
|
|
|
848
|
-
|
|
849
|
-
let
|
|
850
|
-
|
|
851
|
-
|
|
1191
|
+
const targets = {};
|
|
1192
|
+
let allOk = true;
|
|
1193
|
+
for (const name in def.solver.targets) {
|
|
1194
|
+
const spec = def.solver.targets[name];
|
|
1195
|
+
const size = res[spec.res];
|
|
1196
|
+
if (spec.double) {
|
|
1197
|
+
targets[name] = {
|
|
1198
|
+
a: mkTarget(size, spec.fmt, spec.smooth),
|
|
1199
|
+
b: mkTarget(size, spec.fmt, spec.smooth),
|
|
1200
|
+
};
|
|
1201
|
+
allOk = allOk && targets[name].a.ok && targets[name].b.ok;
|
|
1202
|
+
} else {
|
|
1203
|
+
targets[name] = { a: mkTarget(size, spec.fmt, spec.smooth) };
|
|
1204
|
+
allOk = allOk && targets[name].a.ok;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1208
|
+
|
|
1209
|
+
const cleanup = () => made.forEach((t) => {
|
|
1210
|
+
gl.deleteFramebuffer(t.fbo);
|
|
1211
|
+
gl.deleteTexture(t.tex);
|
|
1212
|
+
});
|
|
852
1213
|
|
|
853
|
-
|
|
1214
|
+
if (!allOk) {
|
|
1215
|
+
console.info("[nodality] '" + node.op +
|
|
1216
|
+
"' float targets incomplete - op skipped.");
|
|
1217
|
+
cleanup();
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const progs = {};
|
|
854
1222
|
try {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1223
|
+
for (const name in def.solver.programs) {
|
|
1224
|
+
const pr = gl.createProgram();
|
|
1225
|
+
gl.attachShader(pr, compile(gl.VERTEX_SHADER, VS));
|
|
1226
|
+
gl.attachShader(pr, compile(gl.FRAGMENT_SHADER, def.solver.programs[name]));
|
|
1227
|
+
gl.bindAttribLocation(pr, 0, "a");
|
|
1228
|
+
gl.linkProgram(pr);
|
|
1229
|
+
if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) {
|
|
1230
|
+
throw new Error(name + ": " + gl.getProgramInfoLog(pr));
|
|
1231
|
+
}
|
|
1232
|
+
progs[name] = { prog: pr, loc: {} };
|
|
862
1233
|
}
|
|
863
1234
|
} catch (e) {
|
|
864
|
-
console.warn("[nodality]
|
|
865
|
-
|
|
1235
|
+
console.warn("[nodality] '" + node.op + "' solver programs failed:", e);
|
|
1236
|
+
cleanup();
|
|
1237
|
+
for (const n in progs) gl.deleteProgram(progs[n].prog);
|
|
866
1238
|
return null;
|
|
867
1239
|
}
|
|
868
1240
|
|
|
869
|
-
// Static sim uniforms (set once).
|
|
870
|
-
gl.useProgram(simProg);
|
|
871
|
-
const SU = (n) => gl.getUniformLocation(simProg, n);
|
|
872
|
-
const statics = def.field.uniforms(node);
|
|
873
|
-
for (const key in statics) setUniform(SU(key), statics[key][0], statics[key][1]);
|
|
874
|
-
gl.uniform2f(SU("s_texel"), 1 / res, 1 / res);
|
|
875
|
-
gl.uniform1f(SU("s_enc"), enc);
|
|
876
|
-
gl.uniform1f(SU("s_range"), RANGE);
|
|
877
|
-
|
|
878
|
-
// The main pass needs to know how to decode what it samples.
|
|
879
1241
|
gl.useProgram(prog);
|
|
880
|
-
gl.uniform1f(U(`u${i}_enc`), enc);
|
|
881
|
-
gl.uniform1f(U(`u${i}_range`), RANGE);
|
|
882
|
-
|
|
883
1242
|
return {
|
|
884
|
-
i, res,
|
|
885
|
-
src: pair[0], dst: pair[1],
|
|
886
|
-
force: def.field.force(node),
|
|
1243
|
+
i, node, def, res, targets, progs, cleanup,
|
|
887
1244
|
prevX: 0.5, prevY: 0.5, primed: false,
|
|
888
1245
|
};
|
|
889
1246
|
};
|
|
890
1247
|
rasterNodes.forEach((node, i) => {
|
|
891
1248
|
const def = REGISTRY[node.op];
|
|
892
|
-
if (!def || !def.
|
|
893
|
-
const
|
|
894
|
-
if (
|
|
1249
|
+
if (!def || !def.solver) return;
|
|
1250
|
+
const s = makeSolver(node, i);
|
|
1251
|
+
if (s) solverOps.push(s);
|
|
895
1252
|
});
|
|
896
1253
|
|
|
897
|
-
//
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1254
|
+
// One frame of a solver. `S` is the small API the op's step() drives.
|
|
1255
|
+
const stepSolver = (s, dt) => {
|
|
1256
|
+
let cur = null;
|
|
1257
|
+
const S = {
|
|
1258
|
+
res: s.res,
|
|
1259
|
+
use(name) {
|
|
1260
|
+
cur = s.progs[name];
|
|
1261
|
+
gl.useProgram(cur.prog);
|
|
1262
|
+
},
|
|
1263
|
+
u(name) {
|
|
1264
|
+
if (!(name in cur.loc)) cur.loc[name] = gl.getUniformLocation(cur.prog, name);
|
|
1265
|
+
return cur.loc[name];
|
|
1266
|
+
},
|
|
1267
|
+
f1(n, v) { gl.uniform1f(S.u(n), v); },
|
|
1268
|
+
f2(n, x, y) { gl.uniform2f(S.u(n), x, y); },
|
|
1269
|
+
f3(n, x, y, z) { gl.uniform3f(S.u(n), x, y, z); },
|
|
1270
|
+
smp(n, target, unit) {
|
|
1271
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1272
|
+
gl.bindTexture(gl.TEXTURE_2D, target.tex);
|
|
1273
|
+
gl.uniform1i(S.u(n), unit);
|
|
1274
|
+
},
|
|
1275
|
+
read: (name) => s.targets[name].a,
|
|
1276
|
+
write: (name) => s.targets[name].b,
|
|
1277
|
+
single: (name) => s.targets[name].a,
|
|
1278
|
+
swap(name) {
|
|
1279
|
+
const t = s.targets[name];
|
|
1280
|
+
const tmp = t.a; t.a = t.b; t.b = tmp;
|
|
1281
|
+
},
|
|
1282
|
+
blit(target) {
|
|
1283
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
1284
|
+
gl.viewport(0, 0, target.size, target.size);
|
|
1285
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1286
|
+
},
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// Pointer in field space (bottom-up [0,1]) and the velocity it
|
|
1290
|
+
// picked up since the previous frame - the spoon.
|
|
908
1291
|
const px = mouse[0] / canvas.width;
|
|
909
1292
|
const py = 1 - mouse[1] / canvas.height;
|
|
910
|
-
let
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
1293
|
+
let dx = 0, dy = 0;
|
|
1294
|
+
if (s.primed) {
|
|
1295
|
+
dx = (px - s.prevX) / Math.max(dt, 1e-3);
|
|
1296
|
+
dy = (py - s.prevY) / Math.max(dt, 1e-3);
|
|
914
1297
|
}
|
|
915
|
-
|
|
1298
|
+
s.prevX = px; s.prevY = py; s.primed = true;
|
|
1299
|
+
const moving = Math.abs(dx) + Math.abs(dy) > 1e-3;
|
|
916
1300
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
923
|
-
|
|
924
|
-
const swap = f.src; f.src = f.dst; f.dst = swap;
|
|
1301
|
+
s.def.solver.step(S, s.node, {
|
|
1302
|
+
dt, px, py, dx, dy, moving,
|
|
1303
|
+
aspect: canvas.width / Math.max(canvas.height, 1),
|
|
1304
|
+
col: s.def.solver.splatColor(s.node, dx, dy),
|
|
1305
|
+
});
|
|
925
1306
|
};
|
|
926
1307
|
|
|
927
1308
|
const tex = gl.createTexture();
|
|
@@ -1098,6 +1479,35 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1098
1479
|
el.addEventListener("mousemove", onMove, { passive: true });
|
|
1099
1480
|
el.addEventListener("touchmove", onMove, { passive: true });
|
|
1100
1481
|
|
|
1482
|
+
// Driver inputs beyond the pointer. hoverTarget flips on enter/leave
|
|
1483
|
+
// and `hover` eases toward it, so a hover-driven op resolves away
|
|
1484
|
+
// smoothly instead of snapping off.
|
|
1485
|
+
let hoverTarget = 0, hover = 0;
|
|
1486
|
+
const onEnter = () => { hoverTarget = 1; };
|
|
1487
|
+
const onLeave = () => { hoverTarget = 0; };
|
|
1488
|
+
el.addEventListener("mouseenter", onEnter, { passive: true });
|
|
1489
|
+
el.addEventListener("mouseleave", onLeave, { passive: true });
|
|
1490
|
+
|
|
1491
|
+
// Which nodes need a driver evaluated, and with what.
|
|
1492
|
+
const driverNodes = [];
|
|
1493
|
+
rasterNodes.forEach((node, i) => {
|
|
1494
|
+
const def = REGISTRY[node.op] || {};
|
|
1495
|
+
const name = node.by || def.defaultDriver;
|
|
1496
|
+
if (name && DRIVERS[name]) driverNodes.push({ i, fn: DRIVERS[name] });
|
|
1497
|
+
else if (node.by) {
|
|
1498
|
+
console.warn("[nodality] unknown driver '" + node.by + "' on op '" +
|
|
1499
|
+
node.op + "' - known drivers: " + DRIVER_NAMES.join(", "));
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
// How far the element has travelled across the viewport, 0..1.
|
|
1504
|
+
const scrollProgress = () => {
|
|
1505
|
+
if (typeof window === "undefined") return 0.5;
|
|
1506
|
+
const r = el.getBoundingClientRect();
|
|
1507
|
+
const vh = window.innerHeight || 1;
|
|
1508
|
+
return Math.max(0, Math.min(1, (vh - r.top) / (vh + r.height)));
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1101
1511
|
// Render loop — paused while off-screen.
|
|
1102
1512
|
let raf = 0;
|
|
1103
1513
|
let visible = true;
|
|
@@ -1107,9 +1517,9 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1107
1517
|
const now = (typeof performance !== "undefined") ? performance.now() : lastFrame + 16;
|
|
1108
1518
|
const dt = Math.max(0, (now - lastFrame) / 1000);
|
|
1109
1519
|
|
|
1110
|
-
// 1. Advance stateful
|
|
1111
|
-
//
|
|
1112
|
-
for (const
|
|
1520
|
+
// 1. Advance stateful fluid solvers into their own off-screen
|
|
1521
|
+
// targets (this rebinds framebuffer/program/textures).
|
|
1522
|
+
for (const s of solverOps) stepSolver(s, dt);
|
|
1113
1523
|
|
|
1114
1524
|
// 2. Composite pass, back on the visible framebuffer.
|
|
1115
1525
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
@@ -1120,6 +1530,20 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1120
1530
|
gl.uniform2f(U("u_res"), canvas.width, canvas.height);
|
|
1121
1531
|
gl.uniform2f(U("u_mouse"), mouse[0], mouse[1]);
|
|
1122
1532
|
gl.uniform1f(U("u_time"), now / 1000);
|
|
1533
|
+
// Evaluate each reactive op's driver and upload its focus.
|
|
1534
|
+
if (driverNodes.length > 0) {
|
|
1535
|
+
hover += (hoverTarget - hover) * (1 - Math.exp(-dt * 8));
|
|
1536
|
+
const dctx = {
|
|
1537
|
+
mouseX: mouse[0], mouseY: mouse[1],
|
|
1538
|
+
w: canvas.width, h: canvas.height,
|
|
1539
|
+
t: now / 1000, hover, scroll: scrollProgress(),
|
|
1540
|
+
};
|
|
1541
|
+
for (const d of driverNodes) {
|
|
1542
|
+
const v = d.fn(dctx);
|
|
1543
|
+
gl.uniform2f(U(`u${d.i}_dpos`), v.x, v.y);
|
|
1544
|
+
gl.uniform1f(U(`u${d.i}_damt`), v.amt);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1123
1547
|
// Advance and upload per-frame CPU simulations.
|
|
1124
1548
|
if (dynamicOps.length > 0) {
|
|
1125
1549
|
const ctx = { w: canvas.width, h: canvas.height, mouseX: mouse[0], mouseY: mouse[1], dt: dt, t: now / 1000 };
|
|
@@ -1128,15 +1552,21 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1128
1552
|
for (const u of ups) setUniform(U(`u${d.i}_${u.name}`), u.kind, u.value);
|
|
1129
1553
|
}
|
|
1130
1554
|
}
|
|
1131
|
-
// Content on unit 0, each
|
|
1555
|
+
// Content on unit 0, then each solver's named result
|
|
1556
|
+
// textures (velocity, dye, ...) on units above it.
|
|
1132
1557
|
gl.activeTexture(gl.TEXTURE0);
|
|
1133
1558
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1134
1559
|
gl.uniform1i(U("u_tex"), 0);
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1560
|
+
let unit = 1;
|
|
1561
|
+
for (const s of solverOps) {
|
|
1562
|
+
const smps = s.def.solver.samplers;
|
|
1563
|
+
for (const key in smps) {
|
|
1564
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1565
|
+
gl.bindTexture(gl.TEXTURE_2D, s.targets[smps[key]].a.tex);
|
|
1566
|
+
gl.uniform1i(U(`u${s.i}_${key}`), unit);
|
|
1567
|
+
unit++;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1140
1570
|
gl.activeTexture(gl.TEXTURE0); // texture uploads assume unit 0
|
|
1141
1571
|
lastFrame = now;
|
|
1142
1572
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
@@ -1232,15 +1662,14 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1232
1662
|
if (typeof window !== "undefined") window.removeEventListener("resize", onWinResize);
|
|
1233
1663
|
el.removeEventListener("mousemove", onMove);
|
|
1234
1664
|
el.removeEventListener("touchmove", onMove);
|
|
1665
|
+
el.removeEventListener("mouseenter", onEnter);
|
|
1666
|
+
el.removeEventListener("mouseleave", onLeave);
|
|
1235
1667
|
canvas.removeEventListener("paint", onPaint);
|
|
1236
|
-
for (const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
gl.deleteTexture(t.tex);
|
|
1240
|
-
});
|
|
1241
|
-
gl.deleteProgram(f.prog);
|
|
1668
|
+
for (const s of solverOps) {
|
|
1669
|
+
s.cleanup();
|
|
1670
|
+
for (const n in s.progs) gl.deleteProgram(s.progs[n].prog);
|
|
1242
1671
|
}
|
|
1243
|
-
|
|
1672
|
+
solverOps.length = 0;
|
|
1244
1673
|
while (sourceEl !== el && sourceEl.firstChild) el.appendChild(sourceEl.firstChild);
|
|
1245
1674
|
el.style.visibility = "";
|
|
1246
1675
|
canvas.remove();
|
|
@@ -1248,4 +1677,7 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1248
1677
|
};
|
|
1249
1678
|
}
|
|
1250
1679
|
|
|
1251
|
-
export {
|
|
1680
|
+
export {
|
|
1681
|
+
applyRasterPipeline, registerRasterOp, RASTER_OP_NAMES,
|
|
1682
|
+
isHTMLInCanvasAvailable, DRIVER_NAMES,
|
|
1683
|
+
};
|