nodality 1.0.180 → 1.0.182
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 +954 -33
- 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 +1 -1
package/lib/raster-ops.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.0.
|
|
2
|
+
* nodality v1.0.182
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
// Ops execute in nodes-array order within their stage. Uniforms are
|
|
45
45
|
// namespaced per node index so the same op can appear twice.
|
|
46
46
|
|
|
47
|
+
const MAX_BLOBS = 12;
|
|
48
|
+
|
|
47
49
|
const REGISTRY = {
|
|
48
50
|
hexalize: {
|
|
49
51
|
stage: "cell",
|
|
@@ -124,6 +126,622 @@ const REGISTRY = {
|
|
|
124
126
|
strength: ["1f", node.strength != null ? node.strength : 1.0],
|
|
125
127
|
}),
|
|
126
128
|
},
|
|
129
|
+
|
|
130
|
+
// Stirred liquid: the content swirls as if it were paint being
|
|
131
|
+
// stirred with a spoon. Unlike every other op here this one is
|
|
132
|
+
// STATEFUL — it owns a low-res vector field (a ping-pong pair of
|
|
133
|
+
// render targets) that evolves frame to frame; the main pass just
|
|
134
|
+
// samples the finished field. See the `field` descriptor below and
|
|
135
|
+
// the field-simulation block in applyRasterPipeline().
|
|
136
|
+
//
|
|
137
|
+
// Formulation (deliberately not the usual Stable-Fluids solver):
|
|
138
|
+
// the classic GPU fluid pipeline is advect + vorticity confinement +
|
|
139
|
+
// a divergence/pressure Jacobi solve (Stam 1999; Harris, GPU Gems
|
|
140
|
+
// 2003) — roughly ten passes. This op instead advects momentum along
|
|
141
|
+
// its own flow (semi-Lagrangian backtrace, the one genuinely
|
|
142
|
+
// universal piece) and then ROTATES each momentum vector by the
|
|
143
|
+
// local curl angle. Rotation preserves magnitude, so eddies keep
|
|
144
|
+
// spinning instead of being pushed around, and because it cannot
|
|
145
|
+
// inject divergence energy the field stays stable with no pressure
|
|
146
|
+
// projection at all — the whole simulation is a single pass.
|
|
147
|
+
//
|
|
148
|
+
// { op: "stir", strength: 26, swirl: 2.4, decay: 0.985 }
|
|
149
|
+
// Stirred liquid: an incompressible fluid whose velocity field warps
|
|
150
|
+
// the content and whose dye field colours it.
|
|
151
|
+
//
|
|
152
|
+
// Implemented from the published algorithm, not from any particular
|
|
153
|
+
// existing codebase:
|
|
154
|
+
// J. Stam, "Stable Fluids", SIGGRAPH '99, 121-128 - the
|
|
155
|
+
// unconditionally stable operator splitting used here: add
|
|
156
|
+
// forces, advect by a backward particle trace, then project the
|
|
157
|
+
// velocity onto its divergence-free part via a Poisson solve.
|
|
158
|
+
// M. Harris, "Fast Fluid Dynamics Simulation on the GPU", GPU Gems
|
|
159
|
+
// ch. 38 (2003) - mapping each operator onto a fragment program
|
|
160
|
+
// over ping-ponged textures, Jacobi iterations for the pressure.
|
|
161
|
+
// R. Fedkiw, J. Stam, H. Jensen, "Visual Simulation of Smoke",
|
|
162
|
+
// SIGGRAPH '01 - vorticity confinement, which restores the
|
|
163
|
+
// small-scale swirl that a coarse grid numerically damps away.
|
|
164
|
+
//
|
|
165
|
+
// Per frame: splat -> curl -> confine -> divergence -> pressure
|
|
166
|
+
// (Jacobi) -> subtract gradient -> advect velocity -> advect dye.
|
|
167
|
+
// The projection step is what distinguishes this from a plain
|
|
168
|
+
// velocity-texture smear: enforcing div(v)=0 is what makes the flow
|
|
169
|
+
// roll into persistent vortices instead of piling up and dissipating.
|
|
170
|
+
//
|
|
171
|
+
// Needs WebGL2 with float render targets; without them the op
|
|
172
|
+
// disables itself and the rest of the chain still renders.
|
|
173
|
+
stir: {
|
|
174
|
+
stage: ["warp", "color"],
|
|
175
|
+
decl: (p) => `
|
|
176
|
+
uniform sampler2D ${p}vel;
|
|
177
|
+
uniform sampler2D ${p}dye;
|
|
178
|
+
uniform float ${p}strength;
|
|
179
|
+
uniform float ${p}dyemix;
|
|
180
|
+
uniform float ${p}intensity;
|
|
181
|
+
uniform float ${p}sheen;
|
|
182
|
+
uniform vec3 ${p}tint;`,
|
|
183
|
+
code: (p, node, stage) => stage === "warp" ? `
|
|
184
|
+
{
|
|
185
|
+
// frag/warped are top-down pixels, the field is bottom-up
|
|
186
|
+
// [0,1]: flip y going in, and flip the y of the vector coming
|
|
187
|
+
// out so the content drags WITH the pointer.
|
|
188
|
+
vec2 fuv = vec2(warped.x / u_res.x, 1.0 - warped.y / u_res.y);
|
|
189
|
+
vec2 v = texture2D(${p}vel, clamp(fuv, 0.001, 0.999)).xy;
|
|
190
|
+
warped += vec2(v.x, -v.y) * ${p}strength;
|
|
191
|
+
}` : `
|
|
192
|
+
{
|
|
193
|
+
// Sampled at the CONTINUOUS warped position, never at the
|
|
194
|
+
// cell centre: a cell op upstream (hexalize) snaps the cell
|
|
195
|
+
// centre to one point per tile, which would quantise the
|
|
196
|
+
// liquid into flat per-hex patches instead of smooth swirls.
|
|
197
|
+
vec2 suv = vec2(warped.x / u_res.x, 1.0 - warped.y / u_res.y);
|
|
198
|
+
vec3 dc = texture2D(${p}dye, clamp(suv, 0.001, 0.999)).rgb;
|
|
199
|
+
float mag = length(dc);
|
|
200
|
+
// Colour is carried IN the dye field and advected with it, so
|
|
201
|
+
// a ribbon keeps its hue while the flow folds it. Deriving
|
|
202
|
+
// colour from the local flow direction instead would make
|
|
203
|
+
// neighbouring pixels jump hue and read as confetti.
|
|
204
|
+
vec3 liquid = mag > 1e-4 ? clamp(dc / mag * 1.25, 0.0, 1.0) : ${p}tint;
|
|
205
|
+
float amt = clamp((1.0 - exp(-mag * ${p}intensity)) * ${p}dyemix, 0.0, 1.0);
|
|
206
|
+
// Straight-alpha OVER so a later overlay op (blobs) still
|
|
207
|
+
// composites correctly on top of the liquid.
|
|
208
|
+
float na = amt + ovA * (1.0 - amt);
|
|
209
|
+
ovCol = (liquid * amt + ovCol * ovA * (1.0 - amt)) / max(na, 1e-4);
|
|
210
|
+
ovA = na;
|
|
211
|
+
|
|
212
|
+
// Faint sheen on the content itself where the flow is fast.
|
|
213
|
+
float sp = length(texture2D(${p}vel, clamp(suv, 0.001, 0.999)).xy);
|
|
214
|
+
col = mix(col, ${p}tint, (1.0 - exp(-sp * 0.7)) * ${p}sheen);
|
|
215
|
+
}`,
|
|
216
|
+
uniforms: (node, dpr) => ({
|
|
217
|
+
strength: ["1f", (node.strength != null ? node.strength : 26) * dpr],
|
|
218
|
+
sheen: ["1f", node.sheen != null ? node.sheen : 0.15],
|
|
219
|
+
tint: ["3fv", hexToRgb(node.tint || "#7FD4FF")],
|
|
220
|
+
dyemix: ["1f", node.dye != null ? node.dye : 0.85],
|
|
221
|
+
intensity: ["1f", node.intensity != null ? node.intensity : 1.6],
|
|
222
|
+
}),
|
|
223
|
+
|
|
224
|
+
// ── The simulation ────────────────────────────────────────────
|
|
225
|
+
// Declared as data: the pipeline allocates the targets, compiles
|
|
226
|
+
// the programs, and calls step() once per frame.
|
|
227
|
+
solver: {
|
|
228
|
+
resolutions: (node) => ({
|
|
229
|
+
sim: Math.max(32, Math.min(node.resolution || 128, 512)),
|
|
230
|
+
dye: Math.max(64, Math.min(node.dyeResolution || 512, 1024)),
|
|
231
|
+
}),
|
|
232
|
+
// read/write pairs ping-pong; single targets are scratch.
|
|
233
|
+
targets: {
|
|
234
|
+
velocity: { double: true, res: "sim", fmt: "rg", smooth: true },
|
|
235
|
+
dye: { double: true, res: "dye", fmt: "rgba", smooth: true },
|
|
236
|
+
pressure: { double: true, res: "sim", fmt: "r", smooth: false },
|
|
237
|
+
divergence: { double: false, res: "sim", fmt: "r", smooth: false },
|
|
238
|
+
vorticity: { double: false, res: "sim", fmt: "r", smooth: false },
|
|
239
|
+
},
|
|
240
|
+
// Which target each main-pass sampler reads.
|
|
241
|
+
samplers: { vel: "velocity", dye: "dye" },
|
|
242
|
+
|
|
243
|
+
programs: {
|
|
244
|
+
// Gaussian source term. Distance is measured as a
|
|
245
|
+
// fraction of the element's LONGER side so the radius
|
|
246
|
+
// means the same thing whatever the element's shape.
|
|
247
|
+
splat: `
|
|
248
|
+
precision highp float;
|
|
249
|
+
uniform sampler2D u_src;
|
|
250
|
+
uniform vec2 u_texel;
|
|
251
|
+
uniform vec2 u_point;
|
|
252
|
+
uniform vec3 u_amount;
|
|
253
|
+
uniform float u_radius;
|
|
254
|
+
uniform float u_aspect;
|
|
255
|
+
void main() {
|
|
256
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
257
|
+
vec2 d = uv - u_point;
|
|
258
|
+
if (u_aspect >= 1.0) d.y /= u_aspect; else d.x *= u_aspect;
|
|
259
|
+
float g = exp(-dot(d, d) / max(u_radius * u_radius, 1e-6));
|
|
260
|
+
gl_FragColor = vec4(texture2D(u_src, uv).xyz + u_amount * g, 1.0);
|
|
261
|
+
}`,
|
|
262
|
+
|
|
263
|
+
// Semi-Lagrangian advection (Stam 1999): trace the
|
|
264
|
+
// characteristic backwards and resample. Unconditionally
|
|
265
|
+
// stable for any timestep, which is the whole point.
|
|
266
|
+
advect: `
|
|
267
|
+
precision highp float;
|
|
268
|
+
uniform sampler2D u_vel;
|
|
269
|
+
uniform sampler2D u_src;
|
|
270
|
+
uniform vec2 u_texel;
|
|
271
|
+
uniform float u_dt;
|
|
272
|
+
uniform float u_diss;
|
|
273
|
+
void main() {
|
|
274
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
275
|
+
vec2 back = uv - texture2D(u_vel, uv).xy * u_dt;
|
|
276
|
+
gl_FragColor = vec4(texture2D(u_src, clamp(back, 0.0, 1.0)).xyz * u_diss, 1.0);
|
|
277
|
+
}`,
|
|
278
|
+
|
|
279
|
+
// Scalar vorticity of a 2D field: d(vy)/dx - d(vx)/dy.
|
|
280
|
+
curl: `
|
|
281
|
+
precision highp float;
|
|
282
|
+
uniform sampler2D u_vel;
|
|
283
|
+
uniform vec2 u_texel;
|
|
284
|
+
void main() {
|
|
285
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
286
|
+
float r = texture2D(u_vel, uv + vec2(u_texel.x, 0.0)).y;
|
|
287
|
+
float l = texture2D(u_vel, uv - vec2(u_texel.x, 0.0)).y;
|
|
288
|
+
float t = texture2D(u_vel, uv + vec2(0.0, u_texel.y)).x;
|
|
289
|
+
float b = texture2D(u_vel, uv - vec2(0.0, u_texel.y)).x;
|
|
290
|
+
gl_FragColor = vec4(0.5 * ((r - l) - (t - b)), 0.0, 0.0, 1.0);
|
|
291
|
+
}`,
|
|
292
|
+
|
|
293
|
+
// Vorticity confinement (Fedkiw et al. 2001): push the
|
|
294
|
+
// velocity along N x omega, where N is the unit gradient
|
|
295
|
+
// of |omega|, i.e. toward the centre of each eddy. This
|
|
296
|
+
// feeds energy back into swirl the grid would otherwise
|
|
297
|
+
// smooth out.
|
|
298
|
+
confine: `
|
|
299
|
+
precision highp float;
|
|
300
|
+
uniform sampler2D u_vel;
|
|
301
|
+
uniform sampler2D u_curl;
|
|
302
|
+
uniform vec2 u_texel;
|
|
303
|
+
uniform float u_eps;
|
|
304
|
+
uniform float u_dt;
|
|
305
|
+
void main() {
|
|
306
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
307
|
+
float r = texture2D(u_curl, uv + vec2(u_texel.x, 0.0)).x;
|
|
308
|
+
float l = texture2D(u_curl, uv - vec2(u_texel.x, 0.0)).x;
|
|
309
|
+
float t = texture2D(u_curl, uv + vec2(0.0, u_texel.y)).x;
|
|
310
|
+
float b = texture2D(u_curl, uv - vec2(0.0, u_texel.y)).x;
|
|
311
|
+
float c = texture2D(u_curl, uv).x;
|
|
312
|
+
vec2 grad = 0.5 * vec2(abs(r) - abs(l), abs(t) - abs(b));
|
|
313
|
+
vec2 n = grad / (length(grad) + 1e-5);
|
|
314
|
+
vec2 force = u_eps * c * vec2(n.y, -n.x);
|
|
315
|
+
vec2 v = texture2D(u_vel, uv).xy + force * u_dt;
|
|
316
|
+
gl_FragColor = vec4(clamp(v, -64.0, 64.0), 0.0, 1.0);
|
|
317
|
+
}`,
|
|
318
|
+
|
|
319
|
+
// div(v). The walls reflect, so the fluid stays in the box.
|
|
320
|
+
divergence: `
|
|
321
|
+
precision highp float;
|
|
322
|
+
uniform sampler2D u_vel;
|
|
323
|
+
uniform vec2 u_texel;
|
|
324
|
+
void main() {
|
|
325
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
326
|
+
float r = texture2D(u_vel, uv + vec2(u_texel.x, 0.0)).x;
|
|
327
|
+
float l = texture2D(u_vel, uv - vec2(u_texel.x, 0.0)).x;
|
|
328
|
+
float t = texture2D(u_vel, uv + vec2(0.0, u_texel.y)).y;
|
|
329
|
+
float b = texture2D(u_vel, uv - vec2(0.0, u_texel.y)).y;
|
|
330
|
+
vec2 c = texture2D(u_vel, uv).xy;
|
|
331
|
+
if (uv.x - u_texel.x < 0.0) l = -c.x;
|
|
332
|
+
if (uv.x + u_texel.x > 1.0) r = -c.x;
|
|
333
|
+
if (uv.y - u_texel.y < 0.0) b = -c.y;
|
|
334
|
+
if (uv.y + u_texel.y > 1.0) t = -c.y;
|
|
335
|
+
gl_FragColor = vec4(0.5 * ((r - l) + (t - b)), 0.0, 0.0, 1.0);
|
|
336
|
+
}`,
|
|
337
|
+
|
|
338
|
+
// One Jacobi sweep of the Poisson equation lap(p) = div(v)
|
|
339
|
+
// (Harris 2003). Run a handful of these per frame.
|
|
340
|
+
jacobi: `
|
|
341
|
+
precision highp float;
|
|
342
|
+
uniform sampler2D u_p;
|
|
343
|
+
uniform sampler2D u_div;
|
|
344
|
+
uniform vec2 u_texel;
|
|
345
|
+
void main() {
|
|
346
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
347
|
+
float r = texture2D(u_p, uv + vec2(u_texel.x, 0.0)).x;
|
|
348
|
+
float l = texture2D(u_p, uv - vec2(u_texel.x, 0.0)).x;
|
|
349
|
+
float t = texture2D(u_p, uv + vec2(0.0, u_texel.y)).x;
|
|
350
|
+
float b = texture2D(u_p, uv - vec2(0.0, u_texel.y)).x;
|
|
351
|
+
float d = texture2D(u_div, uv).x;
|
|
352
|
+
gl_FragColor = vec4((r + l + t + b - d) * 0.25, 0.0, 0.0, 1.0);
|
|
353
|
+
}`,
|
|
354
|
+
|
|
355
|
+
// v <- v - grad(p): the projection itself. After this the
|
|
356
|
+
// velocity is (discretely) divergence free.
|
|
357
|
+
subtract: `
|
|
358
|
+
precision highp float;
|
|
359
|
+
uniform sampler2D u_p;
|
|
360
|
+
uniform sampler2D u_vel;
|
|
361
|
+
uniform vec2 u_texel;
|
|
362
|
+
void main() {
|
|
363
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
364
|
+
float r = texture2D(u_p, uv + vec2(u_texel.x, 0.0)).x;
|
|
365
|
+
float l = texture2D(u_p, uv - vec2(u_texel.x, 0.0)).x;
|
|
366
|
+
float t = texture2D(u_p, uv + vec2(0.0, u_texel.y)).x;
|
|
367
|
+
float b = texture2D(u_p, uv - vec2(0.0, u_texel.y)).x;
|
|
368
|
+
vec2 v = texture2D(u_vel, uv).xy - 0.5 * vec2(r - l, t - b);
|
|
369
|
+
gl_FragColor = vec4(v, 0.0, 1.0);
|
|
370
|
+
}`,
|
|
371
|
+
|
|
372
|
+
// Scale a target in place (used to carry a fraction of
|
|
373
|
+
// the previous pressure into the next solve).
|
|
374
|
+
decay: `
|
|
375
|
+
precision highp float;
|
|
376
|
+
uniform sampler2D u_tex;
|
|
377
|
+
uniform vec2 u_texel;
|
|
378
|
+
uniform float u_value;
|
|
379
|
+
void main() {
|
|
380
|
+
gl_FragColor = texture2D(u_tex, gl_FragCoord.xy * u_texel) * u_value;
|
|
381
|
+
}`,
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
// Colour dropped in by the spoon. With rainbow on, the hue
|
|
385
|
+
// comes from the stroke direction at the moment of injection
|
|
386
|
+
// and is then carried by the dye, so it stays coherent as the
|
|
387
|
+
// flow folds it.
|
|
388
|
+
splatColor: (node, dx, dy) => {
|
|
389
|
+
const amt = node.dyeAmount != null ? node.dyeAmount : 0.6;
|
|
390
|
+
if (!node.rainbow) {
|
|
391
|
+
const c = hexToRgb(node.tint || "#7FD4FF");
|
|
392
|
+
return [c[0] * amt, c[1] * amt, c[2] * amt];
|
|
393
|
+
}
|
|
394
|
+
const a = Math.atan2(dy, dx);
|
|
395
|
+
return [
|
|
396
|
+
(0.5 + 0.5 * Math.cos(a)) * amt,
|
|
397
|
+
(0.5 + 0.5 * Math.cos(a + 2.0944)) * amt,
|
|
398
|
+
(0.5 + 0.5 * Math.cos(a + 4.1888)) * amt,
|
|
399
|
+
];
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
step: (S, node, ctx) => {
|
|
403
|
+
const dt = Math.min(ctx.dt, 1 / 30);
|
|
404
|
+
const simT = 1 / S.res.sim;
|
|
405
|
+
const dyeT = 1 / S.res.dye;
|
|
406
|
+
const pow = (base, d) => Math.pow(base, d * 60);
|
|
407
|
+
|
|
408
|
+
// 1. Forces: splat momentum and dye at the pointer.
|
|
409
|
+
if (ctx.moving) {
|
|
410
|
+
const f = node.force != null ? node.force : 1.0;
|
|
411
|
+
S.use("splat");
|
|
412
|
+
S.f1("u_radius", node.radius != null ? node.radius : 0.1);
|
|
413
|
+
S.f1("u_aspect", ctx.aspect);
|
|
414
|
+
S.f2("u_point", ctx.px, ctx.py);
|
|
415
|
+
|
|
416
|
+
S.f2("u_texel", simT, simT);
|
|
417
|
+
S.smp("u_src", S.read("velocity"), 0);
|
|
418
|
+
S.f3("u_amount", ctx.dx * f, ctx.dy * f, 0);
|
|
419
|
+
S.blit(S.write("velocity"));
|
|
420
|
+
S.swap("velocity");
|
|
421
|
+
|
|
422
|
+
S.f2("u_texel", dyeT, dyeT);
|
|
423
|
+
S.smp("u_src", S.read("dye"), 0);
|
|
424
|
+
S.f3("u_amount", ctx.col[0], ctx.col[1], ctx.col[2]);
|
|
425
|
+
S.blit(S.write("dye"));
|
|
426
|
+
S.swap("dye");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 2. Vorticity confinement.
|
|
430
|
+
S.use("curl");
|
|
431
|
+
S.f2("u_texel", simT, simT);
|
|
432
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
433
|
+
S.blit(S.single("vorticity"));
|
|
434
|
+
|
|
435
|
+
S.use("confine");
|
|
436
|
+
S.f2("u_texel", simT, simT);
|
|
437
|
+
S.f1("u_eps", node.curl != null ? node.curl : 2.2);
|
|
438
|
+
S.f1("u_dt", dt);
|
|
439
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
440
|
+
S.smp("u_curl", S.single("vorticity"), 1);
|
|
441
|
+
S.blit(S.write("velocity"));
|
|
442
|
+
S.swap("velocity");
|
|
443
|
+
|
|
444
|
+
// 3. Projection: divergence -> pressure -> subtract.
|
|
445
|
+
S.use("divergence");
|
|
446
|
+
S.f2("u_texel", simT, simT);
|
|
447
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
448
|
+
S.blit(S.single("divergence"));
|
|
449
|
+
|
|
450
|
+
S.use("decay");
|
|
451
|
+
S.f2("u_texel", simT, simT);
|
|
452
|
+
S.f1("u_value", pow(node.pressure != null ? node.pressure : 0.8, dt));
|
|
453
|
+
S.smp("u_tex", S.read("pressure"), 0);
|
|
454
|
+
S.blit(S.write("pressure"));
|
|
455
|
+
S.swap("pressure");
|
|
456
|
+
|
|
457
|
+
S.use("jacobi");
|
|
458
|
+
S.f2("u_texel", simT, simT);
|
|
459
|
+
S.smp("u_div", S.single("divergence"), 1);
|
|
460
|
+
const iters = Math.max(1, Math.min(node.pressureIterations || 6, 40));
|
|
461
|
+
for (let k = 0; k < iters; k++) {
|
|
462
|
+
S.smp("u_p", S.read("pressure"), 0);
|
|
463
|
+
S.blit(S.write("pressure"));
|
|
464
|
+
S.swap("pressure");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
S.use("subtract");
|
|
468
|
+
S.f2("u_texel", simT, simT);
|
|
469
|
+
S.smp("u_p", S.read("pressure"), 0);
|
|
470
|
+
S.smp("u_vel", S.read("velocity"), 1);
|
|
471
|
+
S.blit(S.write("velocity"));
|
|
472
|
+
S.swap("velocity");
|
|
473
|
+
|
|
474
|
+
// 4. Advection: velocity by itself, then dye by velocity.
|
|
475
|
+
S.use("advect");
|
|
476
|
+
S.f1("u_dt", dt);
|
|
477
|
+
S.f2("u_texel", simT, simT);
|
|
478
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
479
|
+
S.smp("u_src", S.read("velocity"), 0);
|
|
480
|
+
S.f1("u_diss", pow(node.decay != null ? node.decay : 0.985, dt));
|
|
481
|
+
S.blit(S.write("velocity"));
|
|
482
|
+
S.swap("velocity");
|
|
483
|
+
|
|
484
|
+
S.f2("u_texel", dyeT, dyeT);
|
|
485
|
+
S.smp("u_vel", S.read("velocity"), 0);
|
|
486
|
+
S.smp("u_src", S.read("dye"), 1);
|
|
487
|
+
S.f1("u_diss", pow(node.dyeFade != null ? node.dyeFade : 0.97, dt));
|
|
488
|
+
S.blit(S.write("dye"));
|
|
489
|
+
S.swap("dye");
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
// Liquid metaballs: a set of moving circles summed into a scalar
|
|
495
|
+
// field; where the field crosses a threshold is "inside" a blob, and
|
|
496
|
+
// neighbouring blobs fuse smoothly (the gooey / liquid look). One
|
|
497
|
+
// blob tracks the pointer; the rest float freely and bounce off the
|
|
498
|
+
// edges. Rendered as a refractive glassy overlay (lens distortion of
|
|
499
|
+
// the content beneath + a rim light + a soft specular glint).
|
|
500
|
+
//
|
|
501
|
+
// Own implementation of the classic metaball technique: the field is
|
|
502
|
+
// Sum(r_i^2 / |p - c_i|^2), and the surface normal is its analytic
|
|
503
|
+
// gradient. init()/tick() run the particle sim on the CPU and feed
|
|
504
|
+
// the positions in as a per-frame uniform array.
|
|
505
|
+
blobs: {
|
|
506
|
+
stage: "color",
|
|
507
|
+
overlay: true,
|
|
508
|
+
mips: true,
|
|
509
|
+
decl: (p) => `
|
|
510
|
+
uniform vec3 ${p}blobs[${MAX_BLOBS}];
|
|
511
|
+
uniform int ${p}count;
|
|
512
|
+
uniform vec3 ${p}tint;
|
|
513
|
+
uniform float ${p}refract;
|
|
514
|
+
uniform float ${p}rim;
|
|
515
|
+
uniform float ${p}alpha;
|
|
516
|
+
uniform float ${p}irid;
|
|
517
|
+
uniform float ${p}disp;
|
|
518
|
+
uniform float ${p}frost;
|
|
519
|
+
float ${p}hash(vec3 v) {
|
|
520
|
+
return fract(sin(dot(v, vec3(21.9898, 63.233, 41.719))) * 41739.317);
|
|
521
|
+
}
|
|
522
|
+
float ${p}noise(vec3 v) {
|
|
523
|
+
vec3 f = floor(v);
|
|
524
|
+
vec3 r = v - f;
|
|
525
|
+
r = r * r * (3.0 - 2.0 * r);
|
|
526
|
+
float c000 = ${p}hash(f);
|
|
527
|
+
float c100 = ${p}hash(f + vec3(1.0, 0.0, 0.0));
|
|
528
|
+
float c010 = ${p}hash(f + vec3(0.0, 1.0, 0.0));
|
|
529
|
+
float c110 = ${p}hash(f + vec3(1.0, 1.0, 0.0));
|
|
530
|
+
float c001 = ${p}hash(f + vec3(0.0, 0.0, 1.0));
|
|
531
|
+
float c101 = ${p}hash(f + vec3(1.0, 0.0, 1.0));
|
|
532
|
+
float c011 = ${p}hash(f + vec3(0.0, 1.0, 1.0));
|
|
533
|
+
float c111 = ${p}hash(f + vec3(1.0, 1.0, 1.0));
|
|
534
|
+
return mix(
|
|
535
|
+
mix(mix(c000, c100, r.x), mix(c010, c110, r.x), r.y),
|
|
536
|
+
mix(mix(c001, c101, r.x), mix(c011, c111, r.x), r.y), r.z);
|
|
537
|
+
}`,
|
|
538
|
+
code: (p) => `
|
|
539
|
+
{
|
|
540
|
+
float field = 0.0;
|
|
541
|
+
vec2 grad = vec2(0.0);
|
|
542
|
+
for (int bi = 0; bi < ${MAX_BLOBS}; bi++) {
|
|
543
|
+
if (bi >= ${p}count) break;
|
|
544
|
+
vec2 bd = frag - ${p}blobs[bi].xy;
|
|
545
|
+
float br = ${p}blobs[bi].z;
|
|
546
|
+
float dd = max(dot(bd, bd), 1.0);
|
|
547
|
+
field += (br * br) / dd;
|
|
548
|
+
grad += (-2.0 * (br * br) / (dd * dd)) * bd;
|
|
549
|
+
}
|
|
550
|
+
float bmask = smoothstep(0.6, 1.0, field);
|
|
551
|
+
if (bmask > 0.003) {
|
|
552
|
+
float gmag = length(grad);
|
|
553
|
+
vec2 g2 = gmag > 1e-6 ? -grad / gmag : vec2(0.0);
|
|
554
|
+
// Pseudo-3D surface normal from the 2D field. For a
|
|
555
|
+
// metaball field (sum of r^2/d^2), 1/sqrt(field) IS the
|
|
556
|
+
// normalized radial position: 0 at the centre, 1 at the
|
|
557
|
+
// surface. That yields a true hemisphere profile — the
|
|
558
|
+
// curvature (and therefore refraction) varies smoothly
|
|
559
|
+
// across the WHOLE disc, not just an edge annulus.
|
|
560
|
+
float s = clamp(inversesqrt(max(field, 1e-4)), 0.0, 1.0);
|
|
561
|
+
vec3 n = normalize(vec3(g2 * s, sqrt(max(1.0 - s * s, 0.004))));
|
|
562
|
+
vec3 rd = vec3(0.0, 0.0, -1.0);
|
|
563
|
+
|
|
564
|
+
// Dispersion: refract each channel with a slightly
|
|
565
|
+
// different index (water-glass ~1.33). Near the rim the
|
|
566
|
+
// exit direction flattens (|z| -> 0), so the sampling
|
|
567
|
+
// offsets stretch — producing the smooth concentric
|
|
568
|
+
// colour-fringed bands of a thick lens.
|
|
569
|
+
float ca = 0.03 * ${p}disp;
|
|
570
|
+
vec3 tR = refract(rd, n, 1.0 / (1.33 - ca));
|
|
571
|
+
vec3 tG = refract(rd, n, 1.0 / 1.33);
|
|
572
|
+
vec3 tB = refract(rd, n, 1.0 / (1.33 + ca));
|
|
573
|
+
vec2 oR = tR.xy * (${p}refract / max(abs(tR.z), 0.3));
|
|
574
|
+
vec2 oG = tG.xy * (${p}refract / max(abs(tG.z), 0.3));
|
|
575
|
+
vec2 oB = tB.xy * (${p}refract / max(abs(tB.z), 0.3));
|
|
576
|
+
|
|
577
|
+
// Blur grows with how far the ray was bent (mip bias), so
|
|
578
|
+
// strongly-refracted regions smear smoothly instead of
|
|
579
|
+
// showing sharp displaced copies. frost adds a base blur.
|
|
580
|
+
float lod = ${p}frost * 5.0 + log2(1.0 + length(oG) * 0.035);
|
|
581
|
+
vec2 px = 1.0 / u_res;
|
|
582
|
+
vec4 sG = texture2D(u_tex, clamp(uv + oG * px, 0.002, 0.998), lod);
|
|
583
|
+
vec3 refr;
|
|
584
|
+
refr.r = texture2D(u_tex, clamp(uv + oR * px, 0.002, 0.998), lod).r;
|
|
585
|
+
refr.g = sG.g;
|
|
586
|
+
refr.b = texture2D(u_tex, clamp(uv + oB * px, 0.002, 0.998), lod).b;
|
|
587
|
+
// Blend in (approximately) linear light for clean fringes.
|
|
588
|
+
refr = pow(max(refr, 0.0), vec3(2.2));
|
|
589
|
+
|
|
590
|
+
vec3 tintLin = pow(${p}tint, vec3(2.2));
|
|
591
|
+
vec3 bodyLin = mix(tintLin * 0.35, refr, sG.a);
|
|
592
|
+
|
|
593
|
+
// Glass darkens toward the silhouette (absorption), the
|
|
594
|
+
// opposite of an additive glow ring.
|
|
595
|
+
float edge = pow(1.0 - clamp(n.z, 0.0, 1.0), 1.5);
|
|
596
|
+
bodyLin *= 1.0 - 0.35 * ${p}rim * edge;
|
|
597
|
+
vec3 body = pow(max(bodyLin, 0.0), vec3(1.0 / 2.2));
|
|
598
|
+
|
|
599
|
+
// Sparse iridescent glints: two drifting value-noise
|
|
600
|
+
// fields sampled on the reflection direction, one warm
|
|
601
|
+
// one cool, sharpened hard so only sparkles survive.
|
|
602
|
+
vec3 rf = reflect(rd, n);
|
|
603
|
+
float nA = ${p}noise(rf * 2.3 + u_time * 0.5);
|
|
604
|
+
float nB = ${p}noise(rf * 2.3 - u_time * 0.5);
|
|
605
|
+
vec3 glint = pow(max(vec3(0.30, 0.52, 0.86) * nA +
|
|
606
|
+
vec3(0.80, 0.56, 0.34) * nB, 0.0), vec3(6.0));
|
|
607
|
+
body += glint * ${p}irid;
|
|
608
|
+
|
|
609
|
+
// Small hard specular from a fixed key light.
|
|
610
|
+
vec3 L = normalize(vec3(-0.5, 0.7, 0.6));
|
|
611
|
+
float spec = pow(max(dot(reflect(-L, n), vec3(0.0, 0.0, 1.0)), 0.0), 60.0);
|
|
612
|
+
body += spec * 0.35;
|
|
613
|
+
|
|
614
|
+
ovCol = body;
|
|
615
|
+
ovA = max(ovA, bmask * ${p}alpha);
|
|
616
|
+
}
|
|
617
|
+
}`,
|
|
618
|
+
uniforms: (node, dpr) => ({
|
|
619
|
+
tint: ["3fv", hexToRgb(node.color || "#EAF4EE")],
|
|
620
|
+
refract: ["1f", (node.refract != null ? node.refract : 70) * dpr],
|
|
621
|
+
rim: ["1f", node.rim != null ? node.rim : 0.6],
|
|
622
|
+
alpha: ["1f", node.alpha != null ? node.alpha : 1.0],
|
|
623
|
+
irid: ["1f", node.iridescence != null ? node.iridescence : 0.8],
|
|
624
|
+
disp: ["1f", node.dispersion != null ? node.dispersion : 1.0],
|
|
625
|
+
frost: ["1f", node.frost != null ? node.frost : 0.25],
|
|
626
|
+
}),
|
|
627
|
+
// CPU particle sim. The cursor blob is a TRAIL of shrinking
|
|
628
|
+
// spheres laid along recent pointer positions: when the pointer
|
|
629
|
+
// rests they coincide (radii normalized so the union is exactly
|
|
630
|
+
// the base circle), when it moves they spread into a gooey
|
|
631
|
+
// teardrop — the kinetic deformation. Extra blobs float freely.
|
|
632
|
+
init: (node, dpr, w, h) => {
|
|
633
|
+
const R = (node.radius || 46) * dpr;
|
|
634
|
+
const nTrail = Math.max(1, Math.min(node.trail != null ? node.trail : 7, MAX_BLOBS));
|
|
635
|
+
// Two radius profiles per trail slot, blended by how stretched
|
|
636
|
+
// the chain currently is:
|
|
637
|
+
// idle — normalized so coincident spheres union to exactly R
|
|
638
|
+
// moving — near-full-size spheres, so the spread chain reads
|
|
639
|
+
// as a fat elastic tube instead of a bead string
|
|
640
|
+
const idleW = [];
|
|
641
|
+
let s2 = 0;
|
|
642
|
+
for (let i = 0; i < nTrail; i++) {
|
|
643
|
+
const wgt = Math.pow(0.86, i);
|
|
644
|
+
idleW.push(wgt);
|
|
645
|
+
s2 += wgt * wgt;
|
|
646
|
+
}
|
|
647
|
+
const norm = Math.sqrt(s2);
|
|
648
|
+
const trailIdleR = idleW.map((wgt) => (R * wgt) / norm);
|
|
649
|
+
const trailMoveR = idleW.map((_, i) => R * 0.85 * Math.pow(0.955, i));
|
|
650
|
+
|
|
651
|
+
const rand = (a, b) => a + Math.random() * (b - a);
|
|
652
|
+
const nDrift = Math.max(0, Math.min((node.count || 1) - 1, MAX_BLOBS - nTrail));
|
|
653
|
+
const drifters = [];
|
|
654
|
+
for (let i = 0; i < nDrift; i++) {
|
|
655
|
+
const ang = rand(0, Math.PI * 2);
|
|
656
|
+
const spd = rand(30, 80) * dpr;
|
|
657
|
+
drifters.push({
|
|
658
|
+
x: rand(R, w - R), y: rand(R, h - R),
|
|
659
|
+
r: R * rand(0.45, 0.8),
|
|
660
|
+
vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
|
|
661
|
+
phase: rand(0, Math.PI * 2),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
const head = { x: w * 0.5, y: h * 0.55 };
|
|
665
|
+
// The chain: one node per trail sphere, all seeded on the head
|
|
666
|
+
// so it starts as a single collapsed circle. seg is the rest
|
|
667
|
+
// gap the tail relaxes toward the leader over.
|
|
668
|
+
const chain = [];
|
|
669
|
+
for (let i = 0; i < nTrail; i++) chain.push({ x: head.x, y: head.y });
|
|
670
|
+
return {
|
|
671
|
+
head, chain, nTrail, drifters,
|
|
672
|
+
trailIdleR, trailMoveR,
|
|
673
|
+
stretch: 0, seg: R * 0.9,
|
|
674
|
+
};
|
|
675
|
+
},
|
|
676
|
+
tick: (state, ctx) => {
|
|
677
|
+
const dt = Math.min(ctx.dt, 0.05);
|
|
678
|
+
const w = ctx.w, h = ctx.h;
|
|
679
|
+
const k = 1 - Math.exp(-dt * 9);
|
|
680
|
+
state.head.x += (ctx.mouseX - state.head.x) * k;
|
|
681
|
+
state.head.y += (ctx.mouseY - state.head.y) * k;
|
|
682
|
+
|
|
683
|
+
// Follow-the-leader: node 0 pins to the head, each following
|
|
684
|
+
// node eases toward the one ahead. At rest the whole chain
|
|
685
|
+
// relaxes onto the head (a single circle); while the pointer
|
|
686
|
+
// moves the tail lags behind and the chain spreads out — that
|
|
687
|
+
// spread is the stretch we measure below.
|
|
688
|
+
const chain = state.chain;
|
|
689
|
+
chain[0].x = state.head.x;
|
|
690
|
+
chain[0].y = state.head.y;
|
|
691
|
+
const follow = 1 - Math.exp(-dt * 16);
|
|
692
|
+
for (let i = 1; i < chain.length; i++) {
|
|
693
|
+
const lead = chain[i - 1], node = chain[i];
|
|
694
|
+
let dx = lead.x - node.x, dy = lead.y - node.y;
|
|
695
|
+
const d = Math.hypot(dx, dy) || 1e-4;
|
|
696
|
+
// Cap the lag so a fast flick can't tear the tube apart,
|
|
697
|
+
// then ease the remainder — gives an elastic pull.
|
|
698
|
+
const maxLag = state.seg;
|
|
699
|
+
if (d > maxLag) { node.x += dx * (1 - maxLag / d); node.y += dy * (1 - maxLag / d); }
|
|
700
|
+
node.x += (lead.x - node.x) * follow;
|
|
701
|
+
node.y += (lead.y - node.y) * follow;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Stretch = how far the tail trails the head, normalised by the
|
|
705
|
+
// chain's natural reach. Smoothed so radii swell/settle softly.
|
|
706
|
+
const tail = chain[chain.length - 1];
|
|
707
|
+
const spread = Math.hypot(tail.x - state.head.x, tail.y - state.head.y);
|
|
708
|
+
const reach = Math.max(1, state.seg * (state.nTrail - 1));
|
|
709
|
+
const target = Math.min(1, spread / reach);
|
|
710
|
+
state.stretch += (target - state.stretch) * (1 - Math.exp(-dt * 8));
|
|
711
|
+
|
|
712
|
+
for (const b of state.drifters) {
|
|
713
|
+
b.x += b.vx * dt;
|
|
714
|
+
b.y += b.vy * dt;
|
|
715
|
+
if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx); }
|
|
716
|
+
else if (b.x > w - b.r) { b.x = w - b.r; b.vx = -Math.abs(b.vx); }
|
|
717
|
+
if (b.y < b.r) { b.y = b.r; b.vy = Math.abs(b.vy); }
|
|
718
|
+
else if (b.y > h - b.r) { b.y = h - b.r; b.vy = -Math.abs(b.vy); }
|
|
719
|
+
b.phase += dt * 1.6;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const arr = new Float32Array(MAX_BLOBS * 3);
|
|
723
|
+
const st = state.stretch;
|
|
724
|
+
for (let i = 0; i < state.nTrail; i++) {
|
|
725
|
+
const p = chain[i];
|
|
726
|
+
arr[i * 3] = p.x;
|
|
727
|
+
arr[i * 3 + 1] = p.y;
|
|
728
|
+
// Idle → normalised radii (spheres union to exactly R);
|
|
729
|
+
// stretched → near-full radii so the spread reads as a fat
|
|
730
|
+
// elastic tube rather than a string of shrinking beads.
|
|
731
|
+
arr[i * 3 + 2] = state.trailIdleR[i] + (state.trailMoveR[i] - state.trailIdleR[i]) * st;
|
|
732
|
+
}
|
|
733
|
+
state.drifters.forEach((b, j) => {
|
|
734
|
+
const i = state.nTrail + j;
|
|
735
|
+
arr[i * 3] = b.x;
|
|
736
|
+
arr[i * 3 + 1] = b.y;
|
|
737
|
+
arr[i * 3 + 2] = b.r * (1.0 + 0.12 * Math.sin(b.phase));
|
|
738
|
+
});
|
|
739
|
+
return [
|
|
740
|
+
{ name: "blobs", kind: "3fv", value: arr },
|
|
741
|
+
{ name: "count", kind: "1i", value: state.nTrail + state.drifters.length },
|
|
742
|
+
];
|
|
743
|
+
},
|
|
744
|
+
},
|
|
127
745
|
};
|
|
128
746
|
|
|
129
747
|
function hexToRgb(c) {
|
|
@@ -157,30 +775,34 @@ function buildFragmentShader(nodes) {
|
|
|
157
775
|
const def = REGISTRY[node.op];
|
|
158
776
|
if (!def) return;
|
|
159
777
|
const p = `u${i}_`;
|
|
160
|
-
decls += def.decl(p) + "\n";
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
778
|
+
decls += def.decl(p, node) + "\n";
|
|
779
|
+
// An op may contribute to more than one stage (stage: ["warp",
|
|
780
|
+
// "color"]) — code() then receives the stage it is emitting for.
|
|
781
|
+
// A plain string stage keeps the original single-snippet form.
|
|
782
|
+
const stages = Array.isArray(def.stage) ? def.stage : [def.stage];
|
|
783
|
+
stages.forEach((st) => {
|
|
784
|
+
const snippet = def.code(p, node, st) + "\n";
|
|
785
|
+
if (st === "warp") warp += snippet;
|
|
786
|
+
else if (st === "cell") cell += snippet;
|
|
787
|
+
else if (st === "displace") displace += snippet;
|
|
788
|
+
else color += snippet;
|
|
789
|
+
});
|
|
166
790
|
});
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
791
|
+
// Compositing, in three layers (all straight-alpha):
|
|
792
|
+
// 1. border layer (edgeCol/edgeCov, from an edges op) UNDER
|
|
793
|
+
// 2. content layer (col over tex.a) then
|
|
794
|
+
// 3. overlay layer (ovCol/ovA, from an overlay op e.g. blobs) ON TOP
|
|
795
|
+
// Absent ops leave their contribution at zero, so this reduces to
|
|
796
|
+
// the plain content for every prior combination. The default seam
|
|
797
|
+
// darkening only applies when a cell op runs without an edges op.
|
|
171
798
|
const hasEdges = nodes.some((n) => n.op === "edges");
|
|
172
|
-
const
|
|
173
|
-
? ` // text (col, tex.a) OVER border (edgeCol, edgeCov): straight alpha
|
|
174
|
-
float outA = tex.a + edgeCov * (1.0 - tex.a);
|
|
175
|
-
vec3 outRGB = (col * tex.a + edgeCol * edgeCov * (1.0 - tex.a)) / max(outA, 1e-4);
|
|
176
|
-
gl_FragColor = vec4(outRGB, outA);`
|
|
177
|
-
: ` col *= 1.0 - edge * 0.55;
|
|
178
|
-
gl_FragColor = vec4(col, tex.a);`;
|
|
799
|
+
const defaultSeam = hasEdges ? "" : " col *= 1.0 - edge * 0.55;";
|
|
179
800
|
return `
|
|
180
801
|
precision highp float;
|
|
181
802
|
uniform sampler2D u_tex;
|
|
182
803
|
uniform vec2 u_res;
|
|
183
804
|
uniform vec2 u_mouse;
|
|
805
|
+
uniform float u_time;
|
|
184
806
|
${decls}
|
|
185
807
|
void main() {
|
|
186
808
|
vec2 frag = vec2(gl_FragCoord.x, u_res.y - gl_FragCoord.y);
|
|
@@ -196,8 +818,17 @@ ${displace}
|
|
|
196
818
|
vec3 col = tex.rgb;
|
|
197
819
|
vec3 edgeCol = vec3(1.0);
|
|
198
820
|
float edgeCov = 0.0;
|
|
821
|
+
vec3 ovCol = vec3(0.0);
|
|
822
|
+
float ovA = 0.0;
|
|
199
823
|
${color}
|
|
200
|
-
${
|
|
824
|
+
${defaultSeam}
|
|
825
|
+
// content OVER border
|
|
826
|
+
float baseA = tex.a + edgeCov * (1.0 - tex.a);
|
|
827
|
+
vec3 baseRGB = (col * tex.a + edgeCol * edgeCov * (1.0 - tex.a)) / max(baseA, 1e-4);
|
|
828
|
+
// overlay OVER (content+border)
|
|
829
|
+
float outA = ovA + baseA * (1.0 - ovA);
|
|
830
|
+
vec3 outRGB = (ovCol * ovA + baseRGB * baseA * (1.0 - ovA)) / max(outA, 1e-4);
|
|
831
|
+
gl_FragColor = vec4(outRGB, outA);
|
|
201
832
|
}`;
|
|
202
833
|
}
|
|
203
834
|
|
|
@@ -247,7 +878,25 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
247
878
|
if (rect.width < 2 || rect.height < 2) return null;
|
|
248
879
|
|
|
249
880
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
250
|
-
const
|
|
881
|
+
const isOverlay = rasterNodes.some((n) => (REGISTRY[n.op] || {}).overlay);
|
|
882
|
+
// Stacking: a pipeline may mix in-place ops (hexalize/offset/duotone/
|
|
883
|
+
// edges) with an overlay op (blobs). The single shader composites them
|
|
884
|
+
// in one pass, but the HOST visibility differs — only a PURELY overlay
|
|
885
|
+
// chain leaves the host visible (the lens refracts untouched content).
|
|
886
|
+
// The moment an in-place op joins, the canvas fully carries the look,
|
|
887
|
+
// so the host must be hidden or the original content ghosts through.
|
|
888
|
+
const pureOverlay = rasterNodes.every((n) => (REGISTRY[n.op] || {}).overlay);
|
|
889
|
+
// Live-first (HTML-in-Canvas). The live backend captures content that
|
|
890
|
+
// lives INSIDE the canvas (texElementImage2D over the restructured
|
|
891
|
+
// subtree) — the same model canvasUI's bubble uses: content in the
|
|
892
|
+
// canvas, the lens drawn over that live capture. So live is viable
|
|
893
|
+
// whenever the host is hidden and its content moves into the canvas,
|
|
894
|
+
// i.e. every chain EXCEPT a pure-overlay one (blobs alone floating
|
|
895
|
+
// over untouched DOM — nothing is restructured, so there is no live
|
|
896
|
+
// subtree to sample; that case stays snapshot). A combined chain
|
|
897
|
+
// (blobs + hexalize/offset/…) hides the host and therefore runs live,
|
|
898
|
+
// lens included. Opt out per pipeline with `live: false` on any node.
|
|
899
|
+
const wantLive = !pureOverlay && !rasterNodes.some((n) => n.live === false);
|
|
251
900
|
const apiAvailable =
|
|
252
901
|
(typeof WebGL2RenderingContext !== "undefined" &&
|
|
253
902
|
"texElementImage2D" in WebGL2RenderingContext.prototype) ||
|
|
@@ -263,11 +912,15 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
263
912
|
// browser prepares the canvas for laid-out children (origin trial).
|
|
264
913
|
if (live) canvas.setAttribute("layoutsubtree", "");
|
|
265
914
|
const ctxOpts = { alpha: true, premultipliedAlpha: false };
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
915
|
+
// WebGL2-first everywhere: NPOT mipmaps (offset-scaled blur for the
|
|
916
|
+
// glassy ops) and texElementImage2D both need it; WebGL1 remains as
|
|
917
|
+
// a sharp-rendering fallback (mip blur simply disables).
|
|
918
|
+
const gl = canvas.getContext("webgl2", ctxOpts) ||
|
|
269
919
|
canvas.getContext("webgl", ctxOpts);
|
|
270
920
|
if (!gl) return null;
|
|
921
|
+
const isGL2 = (typeof WebGL2RenderingContext !== "undefined") &&
|
|
922
|
+
(gl instanceof WebGL2RenderingContext);
|
|
923
|
+
const needMips = isGL2 && rasterNodes.some((n) => (REGISTRY[n.op] || {}).mips);
|
|
271
924
|
if (live && !("texElementImage2D" in gl)) {
|
|
272
925
|
console.info("[nodality] texElementImage2D missing on the created context - snapshot fallback.");
|
|
273
926
|
canvas.removeAttribute("layoutsubtree");
|
|
@@ -324,45 +977,266 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
324
977
|
const prog = gl.createProgram();
|
|
325
978
|
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
|
|
326
979
|
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, buildFragmentShader(rasterNodes)));
|
|
980
|
+
// Pin "a" to location 0 so a field-simulation program (which shares
|
|
981
|
+
// this vertex buffer and attribute array) can be swapped in without
|
|
982
|
+
// respecifying the pointer.
|
|
983
|
+
gl.bindAttribLocation(prog, 0, "a");
|
|
327
984
|
gl.linkProgram(prog);
|
|
328
985
|
gl.useProgram(prog);
|
|
329
986
|
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
330
987
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
331
|
-
|
|
332
|
-
gl.
|
|
333
|
-
gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
|
|
988
|
+
gl.enableVertexAttribArray(0);
|
|
989
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
334
990
|
const U = (n) => gl.getUniformLocation(prog, n);
|
|
335
991
|
|
|
992
|
+
const setUniform = (loc, kind, value) => {
|
|
993
|
+
if (kind === "1f") gl.uniform1f(loc, value);
|
|
994
|
+
else if (kind === "1i") gl.uniform1i(loc, value);
|
|
995
|
+
else if (kind === "2fv") gl.uniform2fv(loc, value);
|
|
996
|
+
else if (kind === "3fv") gl.uniform3fv(loc, value);
|
|
997
|
+
};
|
|
998
|
+
|
|
336
999
|
// Static uniforms from the node data.
|
|
337
1000
|
rasterNodes.forEach((node, i) => {
|
|
338
1001
|
const def = REGISTRY[node.op];
|
|
339
1002
|
if (!def || !def.uniforms) return;
|
|
340
1003
|
const us = def.uniforms(node, dpr);
|
|
341
1004
|
for (const key in us) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1005
|
+
setUniform(U(`u${i}_${key}`), us[key][0], us[key][1]);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// Ops with a CPU simulation (e.g. blobs) get per-frame state and
|
|
1010
|
+
// feed dynamic uniforms each draw (see the render loop below).
|
|
1011
|
+
const dynamicOps = [];
|
|
1012
|
+
rasterNodes.forEach((node, i) => {
|
|
1013
|
+
const def = REGISTRY[node.op];
|
|
1014
|
+
if (def && typeof def.init === "function") {
|
|
1015
|
+
dynamicOps.push({ i, def, state: def.init(node, dpr, canvas.width, canvas.height) });
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
let lastFrame = (typeof performance !== "undefined") ? performance.now() : 0;
|
|
1019
|
+
|
|
1020
|
+
// ── Fluid solvers (ops with a `solver` descriptor, e.g. stir) ─────
|
|
1021
|
+
// An op may declare a set of render targets and fragment programs
|
|
1022
|
+
// plus a step() that sequences the passes. The pipeline allocates
|
|
1023
|
+
// everything here, runs step() once per frame, and binds the named
|
|
1024
|
+
// result textures into the main composite pass.
|
|
1025
|
+
//
|
|
1026
|
+
// Float render targets are required (velocity and pressure are
|
|
1027
|
+
// signed and need range well outside [0,1]). Without them the op
|
|
1028
|
+
// disables itself rather than rendering something wrong.
|
|
1029
|
+
const solverOps = [];
|
|
1030
|
+
const makeSolver = (node, i) => {
|
|
1031
|
+
const def = REGISTRY[node.op];
|
|
1032
|
+
if (!isGL2 || !gl.getExtension("EXT_color_buffer_float")) {
|
|
1033
|
+
console.info("[nodality] '" + node.op +
|
|
1034
|
+
"' needs WebGL2 float render targets - op skipped.");
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
gl.getExtension("OES_texture_float_linear");
|
|
1038
|
+
const res = def.solver.resolutions(node);
|
|
1039
|
+
const FMT = {
|
|
1040
|
+
r: [gl.R16F, gl.RED],
|
|
1041
|
+
rg: [gl.RG16F, gl.RG],
|
|
1042
|
+
rgba: [gl.RGBA16F, gl.RGBA],
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const made = [];
|
|
1046
|
+
const mkTarget = (size, fmt, smooth) => {
|
|
1047
|
+
const [internal, format] = FMT[fmt];
|
|
1048
|
+
const t = gl.createTexture();
|
|
1049
|
+
gl.bindTexture(gl.TEXTURE_2D, t);
|
|
1050
|
+
const flt = smooth ? gl.LINEAR : gl.NEAREST;
|
|
1051
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, flt);
|
|
1052
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, flt);
|
|
1053
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1054
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1055
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internal, size, size, 0,
|
|
1056
|
+
format, gl.HALF_FLOAT, null);
|
|
1057
|
+
const fbo = gl.createFramebuffer();
|
|
1058
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1059
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
|
|
1060
|
+
gl.TEXTURE_2D, t, 0);
|
|
1061
|
+
const ok = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
1062
|
+
gl.viewport(0, 0, size, size);
|
|
1063
|
+
gl.clearColor(0, 0, 0, 1);
|
|
1064
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1065
|
+
const rec = { tex: t, fbo, size, ok };
|
|
1066
|
+
made.push(rec);
|
|
1067
|
+
return rec;
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
const targets = {};
|
|
1071
|
+
let allOk = true;
|
|
1072
|
+
for (const name in def.solver.targets) {
|
|
1073
|
+
const spec = def.solver.targets[name];
|
|
1074
|
+
const size = res[spec.res];
|
|
1075
|
+
if (spec.double) {
|
|
1076
|
+
targets[name] = {
|
|
1077
|
+
a: mkTarget(size, spec.fmt, spec.smooth),
|
|
1078
|
+
b: mkTarget(size, spec.fmt, spec.smooth),
|
|
1079
|
+
};
|
|
1080
|
+
allOk = allOk && targets[name].a.ok && targets[name].b.ok;
|
|
1081
|
+
} else {
|
|
1082
|
+
targets[name] = { a: mkTarget(size, spec.fmt, spec.smooth) };
|
|
1083
|
+
allOk = allOk && targets[name].a.ok;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1087
|
+
|
|
1088
|
+
const cleanup = () => made.forEach((t) => {
|
|
1089
|
+
gl.deleteFramebuffer(t.fbo);
|
|
1090
|
+
gl.deleteTexture(t.tex);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
if (!allOk) {
|
|
1094
|
+
console.info("[nodality] '" + node.op +
|
|
1095
|
+
"' float targets incomplete - op skipped.");
|
|
1096
|
+
cleanup();
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const progs = {};
|
|
1101
|
+
try {
|
|
1102
|
+
for (const name in def.solver.programs) {
|
|
1103
|
+
const pr = gl.createProgram();
|
|
1104
|
+
gl.attachShader(pr, compile(gl.VERTEX_SHADER, VS));
|
|
1105
|
+
gl.attachShader(pr, compile(gl.FRAGMENT_SHADER, def.solver.programs[name]));
|
|
1106
|
+
gl.bindAttribLocation(pr, 0, "a");
|
|
1107
|
+
gl.linkProgram(pr);
|
|
1108
|
+
if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) {
|
|
1109
|
+
throw new Error(name + ": " + gl.getProgramInfoLog(pr));
|
|
1110
|
+
}
|
|
1111
|
+
progs[name] = { prog: pr, loc: {} };
|
|
1112
|
+
}
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
console.warn("[nodality] '" + node.op + "' solver programs failed:", e);
|
|
1115
|
+
cleanup();
|
|
1116
|
+
for (const n in progs) gl.deleteProgram(progs[n].prog);
|
|
1117
|
+
return null;
|
|
347
1118
|
}
|
|
1119
|
+
|
|
1120
|
+
gl.useProgram(prog);
|
|
1121
|
+
return {
|
|
1122
|
+
i, node, def, res, targets, progs, cleanup,
|
|
1123
|
+
prevX: 0.5, prevY: 0.5, primed: false,
|
|
1124
|
+
};
|
|
1125
|
+
};
|
|
1126
|
+
rasterNodes.forEach((node, i) => {
|
|
1127
|
+
const def = REGISTRY[node.op];
|
|
1128
|
+
if (!def || !def.solver) return;
|
|
1129
|
+
const s = makeSolver(node, i);
|
|
1130
|
+
if (s) solverOps.push(s);
|
|
348
1131
|
});
|
|
349
1132
|
|
|
1133
|
+
// One frame of a solver. `S` is the small API the op's step() drives.
|
|
1134
|
+
const stepSolver = (s, dt) => {
|
|
1135
|
+
let cur = null;
|
|
1136
|
+
const S = {
|
|
1137
|
+
res: s.res,
|
|
1138
|
+
use(name) {
|
|
1139
|
+
cur = s.progs[name];
|
|
1140
|
+
gl.useProgram(cur.prog);
|
|
1141
|
+
},
|
|
1142
|
+
u(name) {
|
|
1143
|
+
if (!(name in cur.loc)) cur.loc[name] = gl.getUniformLocation(cur.prog, name);
|
|
1144
|
+
return cur.loc[name];
|
|
1145
|
+
},
|
|
1146
|
+
f1(n, v) { gl.uniform1f(S.u(n), v); },
|
|
1147
|
+
f2(n, x, y) { gl.uniform2f(S.u(n), x, y); },
|
|
1148
|
+
f3(n, x, y, z) { gl.uniform3f(S.u(n), x, y, z); },
|
|
1149
|
+
smp(n, target, unit) {
|
|
1150
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1151
|
+
gl.bindTexture(gl.TEXTURE_2D, target.tex);
|
|
1152
|
+
gl.uniform1i(S.u(n), unit);
|
|
1153
|
+
},
|
|
1154
|
+
read: (name) => s.targets[name].a,
|
|
1155
|
+
write: (name) => s.targets[name].b,
|
|
1156
|
+
single: (name) => s.targets[name].a,
|
|
1157
|
+
swap(name) {
|
|
1158
|
+
const t = s.targets[name];
|
|
1159
|
+
const tmp = t.a; t.a = t.b; t.b = tmp;
|
|
1160
|
+
},
|
|
1161
|
+
blit(target) {
|
|
1162
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
1163
|
+
gl.viewport(0, 0, target.size, target.size);
|
|
1164
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1165
|
+
},
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// Pointer in field space (bottom-up [0,1]) and the velocity it
|
|
1169
|
+
// picked up since the previous frame - the spoon.
|
|
1170
|
+
const px = mouse[0] / canvas.width;
|
|
1171
|
+
const py = 1 - mouse[1] / canvas.height;
|
|
1172
|
+
let dx = 0, dy = 0;
|
|
1173
|
+
if (s.primed) {
|
|
1174
|
+
dx = (px - s.prevX) / Math.max(dt, 1e-3);
|
|
1175
|
+
dy = (py - s.prevY) / Math.max(dt, 1e-3);
|
|
1176
|
+
}
|
|
1177
|
+
s.prevX = px; s.prevY = py; s.primed = true;
|
|
1178
|
+
const moving = Math.abs(dx) + Math.abs(dy) > 1e-3;
|
|
1179
|
+
|
|
1180
|
+
s.def.solver.step(S, s.node, {
|
|
1181
|
+
dt, px, py, dx, dy, moving,
|
|
1182
|
+
aspect: canvas.width / Math.max(canvas.height, 1),
|
|
1183
|
+
col: s.def.solver.splatColor(s.node, dx, dy),
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1186
|
+
|
|
350
1187
|
const tex = gl.createTexture();
|
|
351
1188
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
352
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
|
|
1189
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
|
|
1190
|
+
needMips ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
|
|
353
1191
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
354
1192
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
355
1193
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
1194
|
+
// Regenerate the mip chain after every content upload so the
|
|
1195
|
+
// offset-scaled blur (texture2D bias) has levels to sample.
|
|
1196
|
+
const finishUpload = () => { if (needMips) gl.generateMipmap(gl.TEXTURE_2D); };
|
|
356
1197
|
|
|
357
1198
|
let textureReady = false;
|
|
358
1199
|
let destroyed = false;
|
|
359
1200
|
let mode = live ? "live" : "snapshot";
|
|
360
1201
|
|
|
1202
|
+
// For an overlay effect whose content is (or contains) a same-origin
|
|
1203
|
+
// image, upload that image straight into the texture. This bypasses
|
|
1204
|
+
// the foreignObject snapshot (which can't rasterise <img>) and gives
|
|
1205
|
+
// the lens a real photo to refract — the glassy, distorting look.
|
|
1206
|
+
const contentImg = () => {
|
|
1207
|
+
const im = (el.tagName === "IMG") ? el
|
|
1208
|
+
: (el.querySelector ? el.querySelector("img") : null);
|
|
1209
|
+
return (im && im.complete && im.naturalWidth > 0) ? im : null;
|
|
1210
|
+
};
|
|
1211
|
+
let imgListenerBound = false;
|
|
1212
|
+
|
|
361
1213
|
// Snapshot the host's children (excluding our own canvas). Once the
|
|
362
1214
|
// texture is live, the host's own painting is hidden via the
|
|
363
1215
|
// visibility trick (the canvas child re-shows itself) so transparent
|
|
364
1216
|
// hosts — e.g. a bare headline — don't ghost-double under the effect.
|
|
365
1217
|
const snapshotCapture = () => {
|
|
1218
|
+
// Overlay + image content: use the image directly as the texture.
|
|
1219
|
+
if (isOverlay) {
|
|
1220
|
+
const im = contentImg();
|
|
1221
|
+
if (im) {
|
|
1222
|
+
try {
|
|
1223
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1224
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, im);
|
|
1225
|
+
finishUpload();
|
|
1226
|
+
textureReady = true;
|
|
1227
|
+
return Promise.resolve();
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
// Cross-origin taint -> fall through to snapshot.
|
|
1230
|
+
}
|
|
1231
|
+
} else if (!imgListenerBound) {
|
|
1232
|
+
// Image not decoded yet: recapture once it loads.
|
|
1233
|
+
const im2 = (el.tagName === "IMG") ? el : (el.querySelector && el.querySelector("img"));
|
|
1234
|
+
if (im2) {
|
|
1235
|
+
imgListenerBound = true;
|
|
1236
|
+
im2.addEventListener("load", () => { if (!destroyed) snapshotCapture(); }, { once: true });
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
366
1240
|
canvas.style.display = "none";
|
|
367
1241
|
const prevVisibility = el.style.visibility;
|
|
368
1242
|
el.style.visibility = "";
|
|
@@ -371,8 +1245,13 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
371
1245
|
if (destroyed) return;
|
|
372
1246
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
373
1247
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
|
|
1248
|
+
finishUpload();
|
|
374
1249
|
textureReady = true;
|
|
375
|
-
|
|
1250
|
+
// In-place transforms hide the host (the canvas replaces
|
|
1251
|
+
// its look). A pure-overlay chain keeps it visible so the
|
|
1252
|
+
// real content shows through the bubble; a mixed chain
|
|
1253
|
+
// (in-place + overlay) hides it — the canvas carries all.
|
|
1254
|
+
el.style.visibility = pureOverlay ? "" : "hidden";
|
|
376
1255
|
canvas.style.visibility = "visible";
|
|
377
1256
|
})
|
|
378
1257
|
.catch((e) => {
|
|
@@ -422,6 +1301,7 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
422
1301
|
for (const i of order) {
|
|
423
1302
|
try {
|
|
424
1303
|
gl.texElementImage2D.apply(gl, forms[i]);
|
|
1304
|
+
finishUpload();
|
|
425
1305
|
if (liveForm < 0) {
|
|
426
1306
|
liveForm = i;
|
|
427
1307
|
console.info("[nodality] texElementImage2D signature discovered: form",
|
|
@@ -484,11 +1364,47 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
484
1364
|
const draw = () => {
|
|
485
1365
|
if (destroyed) return;
|
|
486
1366
|
if (visible && textureReady) {
|
|
1367
|
+
const now = (typeof performance !== "undefined") ? performance.now() : lastFrame + 16;
|
|
1368
|
+
const dt = Math.max(0, (now - lastFrame) / 1000);
|
|
1369
|
+
|
|
1370
|
+
// 1. Advance stateful fluid solvers into their own off-screen
|
|
1371
|
+
// targets (this rebinds framebuffer/program/textures).
|
|
1372
|
+
for (const s of solverOps) stepSolver(s, dt);
|
|
1373
|
+
|
|
1374
|
+
// 2. Composite pass, back on the visible framebuffer.
|
|
1375
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1376
|
+
gl.useProgram(prog);
|
|
487
1377
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
488
1378
|
gl.clearColor(0, 0, 0, 0);
|
|
489
1379
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
490
1380
|
gl.uniform2f(U("u_res"), canvas.width, canvas.height);
|
|
491
1381
|
gl.uniform2f(U("u_mouse"), mouse[0], mouse[1]);
|
|
1382
|
+
gl.uniform1f(U("u_time"), now / 1000);
|
|
1383
|
+
// Advance and upload per-frame CPU simulations.
|
|
1384
|
+
if (dynamicOps.length > 0) {
|
|
1385
|
+
const ctx = { w: canvas.width, h: canvas.height, mouseX: mouse[0], mouseY: mouse[1], dt: dt, t: now / 1000 };
|
|
1386
|
+
for (const d of dynamicOps) {
|
|
1387
|
+
const ups = d.def.tick(d.state, ctx);
|
|
1388
|
+
for (const u of ups) setUniform(U(`u${d.i}_${u.name}`), u.kind, u.value);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// Content on unit 0, then each solver's named result
|
|
1392
|
+
// textures (velocity, dye, ...) on units above it.
|
|
1393
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1394
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1395
|
+
gl.uniform1i(U("u_tex"), 0);
|
|
1396
|
+
let unit = 1;
|
|
1397
|
+
for (const s of solverOps) {
|
|
1398
|
+
const smps = s.def.solver.samplers;
|
|
1399
|
+
for (const key in smps) {
|
|
1400
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1401
|
+
gl.bindTexture(gl.TEXTURE_2D, s.targets[smps[key]].a.tex);
|
|
1402
|
+
gl.uniform1i(U(`u${s.i}_${key}`), unit);
|
|
1403
|
+
unit++;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
gl.activeTexture(gl.TEXTURE0); // texture uploads assume unit 0
|
|
1407
|
+
lastFrame = now;
|
|
492
1408
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
493
1409
|
}
|
|
494
1410
|
raf = requestAnimationFrame(draw);
|
|
@@ -583,6 +1499,11 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
583
1499
|
el.removeEventListener("mousemove", onMove);
|
|
584
1500
|
el.removeEventListener("touchmove", onMove);
|
|
585
1501
|
canvas.removeEventListener("paint", onPaint);
|
|
1502
|
+
for (const s of solverOps) {
|
|
1503
|
+
s.cleanup();
|
|
1504
|
+
for (const n in s.progs) gl.deleteProgram(s.progs[n].prog);
|
|
1505
|
+
}
|
|
1506
|
+
solverOps.length = 0;
|
|
586
1507
|
while (sourceEl !== el && sourceEl.firstChild) el.appendChild(sourceEl.firstChild);
|
|
587
1508
|
el.style.visibility = "";
|
|
588
1509
|
canvas.remove();
|