nodality 1.0.187 → 1.0.188
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 +46 -12
- 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 +15 -4
- 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 +556 -46
- 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.188
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -57,6 +57,10 @@ const MAX_BLOBS = 12;
|
|
|
57
57
|
// 0..1 that the op scales its effect by. Ops read them as u<i>_dpos and
|
|
58
58
|
// u<i>_damt; the pipeline evaluates and uploads them once per frame.
|
|
59
59
|
const DRIVERS = {
|
|
60
|
+
// Centre of the element at full strength. The default for ops that
|
|
61
|
+
// need a focus point but were not given a `by:` -- without it their
|
|
62
|
+
// dpos/damt would never be uploaded and would read as zero.
|
|
63
|
+
static: (c) => ({ x: c.w * 0.5, y: c.h * 0.5, amt: 1 }),
|
|
60
64
|
mouse: (c) => ({ x: c.mouseX, y: c.mouseY, amt: 1 }),
|
|
61
65
|
// Same focus as mouse, but faded in and out with the hover state, so
|
|
62
66
|
// the effect resolves away when the pointer leaves.
|
|
@@ -259,6 +263,280 @@ const REGISTRY = {
|
|
|
259
263
|
}),
|
|
260
264
|
},
|
|
261
265
|
|
|
266
|
+
// ── Field producers ──────────────────────────────────────────────
|
|
267
|
+
// These draw nothing. They write a scalar field that LATER ops read
|
|
268
|
+
// via `masked:`, which is the whole of the node-to-node data flow:
|
|
269
|
+
//
|
|
270
|
+
// { op: "mask", from: "radial", as: "centre", radius: 300 },
|
|
271
|
+
// { op: "stir", masked: "centre" }, // fluid only in the middle
|
|
272
|
+
// { op: "halftone" }, // screen everywhere
|
|
273
|
+
//
|
|
274
|
+
// The masking wrapper is generic, so an op does not know or care
|
|
275
|
+
// that it is being constrained — every op, including ones written
|
|
276
|
+
// before fields existed, can be masked.
|
|
277
|
+
|
|
278
|
+
// Geometric / content masks.
|
|
279
|
+
mask: {
|
|
280
|
+
stage: "field",
|
|
281
|
+
producesField: true,
|
|
282
|
+
defaultDriver: "static",
|
|
283
|
+
decl: (p) => `
|
|
284
|
+
uniform vec2 ${p}dpos;
|
|
285
|
+
uniform float ${p}damt;
|
|
286
|
+
uniform float ${p}radius;
|
|
287
|
+
uniform vec2 ${p}at;
|
|
288
|
+
uniform vec2 ${p}remap;
|
|
289
|
+
uniform float ${p}invert;`,
|
|
290
|
+
code: (p, node) => {
|
|
291
|
+
const src = node.from || "radial";
|
|
292
|
+
// Where a radial mask sits. `at:` is fractional (0..1 of the
|
|
293
|
+
// element box) so it survives resizes; without it the focus
|
|
294
|
+
// comes from the driver, which defaults to the centre.
|
|
295
|
+
const ctr = node.at ? `(${p}at * u_res)` : `${p}dpos`;
|
|
296
|
+
const raw = src === "vertical"
|
|
297
|
+
? `frag.y / max(u_res.y, 1.0)`
|
|
298
|
+
: src === "horizontal"
|
|
299
|
+
? `frag.x / max(u_res.x, 1.0)`
|
|
300
|
+
: /* radial, and the default */
|
|
301
|
+
`1.0 - smoothstep(0.0, ${p}radius, length(frag - ${ctr}))`;
|
|
302
|
+
return `
|
|
303
|
+
{
|
|
304
|
+
float m = ${raw};
|
|
305
|
+
m = clamp((m - ${p}remap.x) / max(${p}remap.y - ${p}remap.x, 1e-4), 0.0, 1.0);
|
|
306
|
+
m = mix(m, 1.0 - m, ${p}invert);
|
|
307
|
+
${fieldVar(node.as || "mask")} *= m * ${p}damt;
|
|
308
|
+
}`;
|
|
309
|
+
},
|
|
310
|
+
uniforms: (node, dpr) => ({
|
|
311
|
+
radius: ["1f", (node.radius != null ? node.radius : 260) * dpr],
|
|
312
|
+
at: ["2fv", node.at || [0.5, 0.5]],
|
|
313
|
+
remap: ["2fv", node.remap || [0, 1]],
|
|
314
|
+
invert: ["1f", node.invert ? 1 : 0],
|
|
315
|
+
}),
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
// Animated value noise as a field. The Houdini move: make an
|
|
319
|
+
// attribute, then reference it downstream.
|
|
320
|
+
//
|
|
321
|
+
// { op: "noise", as: "turbulence", scale: 3, speed: 0.4 },
|
|
322
|
+
// { op: "offset", masked: "turbulence", strength: 30 },
|
|
323
|
+
noise: {
|
|
324
|
+
stage: "field",
|
|
325
|
+
producesField: true,
|
|
326
|
+
decl: (p) => `
|
|
327
|
+
uniform float ${p}scale;
|
|
328
|
+
uniform float ${p}speed;
|
|
329
|
+
uniform vec2 ${p}remap;
|
|
330
|
+
uniform float ${p}amp;
|
|
331
|
+
float ${p}h(vec2 v) {
|
|
332
|
+
return fract(sin(dot(v, vec2(41.113, 289.717))) * 43758.545);
|
|
333
|
+
}
|
|
334
|
+
float ${p}n(vec2 v) {
|
|
335
|
+
vec2 i = floor(v), f = v - i;
|
|
336
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
337
|
+
return mix(mix(${p}h(i), ${p}h(i + vec2(1.0, 0.0)), f.x),
|
|
338
|
+
mix(${p}h(i + vec2(0.0, 1.0)), ${p}h(i + vec2(1.0, 1.0)), f.x), f.y);
|
|
339
|
+
}`,
|
|
340
|
+
code: (p, node) => `
|
|
341
|
+
{
|
|
342
|
+
vec2 np = frag / max(u_res.y, 1.0) * ${p}scale;
|
|
343
|
+
// Two octaves: enough structure to read as turbulence
|
|
344
|
+
// without the cost of a full fBm.
|
|
345
|
+
float n = ${p}n(np + u_time * ${p}speed) * 0.65 +
|
|
346
|
+
${p}n(np * 2.7 - u_time * ${p}speed * 0.6) * 0.35;
|
|
347
|
+
n = clamp((n - ${p}remap.x) / max(${p}remap.y - ${p}remap.x, 1e-4), 0.0, 1.0);
|
|
348
|
+
${fieldVar(node.as || "mask")} *= mix(1.0, n, ${p}amp);
|
|
349
|
+
}`,
|
|
350
|
+
uniforms: (node) => ({
|
|
351
|
+
scale: ["1f", node.scale != null ? node.scale : 3],
|
|
352
|
+
speed: ["1f", node.speed != null ? node.speed : 0.3],
|
|
353
|
+
remap: ["2fv", node.remap || [0, 1]],
|
|
354
|
+
amp: ["1f", node.amount != null ? node.amount : 1],
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// Copy to points. The content is re-sampled at a set of points, each
|
|
359
|
+
// stamp scaled and rotated, and composited into the overlay layer so
|
|
360
|
+
// the copies sit above the original rather than replacing it.
|
|
361
|
+
//
|
|
362
|
+
// { op: "copy", count: 6, radius: 120, scale: 0.55, rotate: 0.3 }
|
|
363
|
+
// { op: "copy", points: [[0.2, 0.5], [0.8, 0.5]], scale: 0.4 }
|
|
364
|
+
//
|
|
365
|
+
// With `count`, the points are a ring around the driver focus, so
|
|
366
|
+
// `by: "mouse"` drags the whole instance set around. With explicit
|
|
367
|
+
// `points`, positions are fractional (0..1 of the element box) and
|
|
368
|
+
// survive resizes.
|
|
369
|
+
//
|
|
370
|
+
// Single pass, so the stamp count is fixed at compile time and the
|
|
371
|
+
// loop is unrolled: `count` is a structural parameter, not something
|
|
372
|
+
// to animate. Copies sample the ORIGINAL texture, not the result of
|
|
373
|
+
// earlier colour ops, so a copy of a halftoned element is a copy of
|
|
374
|
+
// the unhalftoned source with the screen applied over it.
|
|
375
|
+
copy: {
|
|
376
|
+
stage: "color",
|
|
377
|
+
defaultDriver: "static",
|
|
378
|
+
decl: (p) => `
|
|
379
|
+
uniform vec2 ${p}dpos;
|
|
380
|
+
uniform float ${p}damt;
|
|
381
|
+
uniform float ${p}radius;
|
|
382
|
+
uniform float ${p}scale;
|
|
383
|
+
uniform float ${p}rotate;
|
|
384
|
+
uniform float ${p}fade;
|
|
385
|
+
uniform float ${p}amount;`,
|
|
386
|
+
code: (p, node) => {
|
|
387
|
+
const explicit = Array.isArray(node.points) && node.points.length > 0;
|
|
388
|
+
const n = explicit
|
|
389
|
+
? node.points.length
|
|
390
|
+
: Math.max(1, Math.min(24, Math.round(node.count || 5)));
|
|
391
|
+
let body = "";
|
|
392
|
+
for (let k = 0; k < n; k++) {
|
|
393
|
+
// Where this stamp is centred.
|
|
394
|
+
const centre = explicit
|
|
395
|
+
? `vec2(${(+node.points[k][0]).toFixed(5)}, ${(+node.points[k][1]).toFixed(5)}) * u_res`
|
|
396
|
+
: (() => {
|
|
397
|
+
const a = (k / n) * Math.PI * 2;
|
|
398
|
+
return `${p}dpos + vec2(${Math.cos(a).toFixed(5)}, ` +
|
|
399
|
+
`${Math.sin(a).toFixed(5)}) * ${p}radius`;
|
|
400
|
+
})();
|
|
401
|
+
body += `
|
|
402
|
+
{
|
|
403
|
+
float ang = ${p}rotate * ${(k + 1).toFixed(1)};
|
|
404
|
+
float cs = cos(ang), sn = sin(ang);
|
|
405
|
+
// Inverse transform: to draw a stamp scaled by s about a
|
|
406
|
+
// centre c, fetch from the source at (frag - c) / s.
|
|
407
|
+
vec2 d = mat2(cs, -sn, sn, cs) * (frag - (${centre}));
|
|
408
|
+
vec2 q = d / max(${p}scale, 1e-3) + u_res * 0.5;
|
|
409
|
+
vec2 quv = vec2(q.x / u_res.x, 1.0 - q.y / u_res.y);
|
|
410
|
+
vec2 inside = step(vec2(0.0), quv) * step(quv, vec2(1.0));
|
|
411
|
+
vec4 s = texture2D(u_tex, clamp(quv, 0.0, 1.0));
|
|
412
|
+
float w = s.a * inside.x * inside.y * ${p}amount * ${p}damt
|
|
413
|
+
* pow(${p}fade, ${k.toFixed(1)});
|
|
414
|
+
// Straight-alpha OVER, newest stamp on top.
|
|
415
|
+
float na = w + ovA * (1.0 - w);
|
|
416
|
+
ovCol = (s.rgb * w + ovCol * ovA * (1.0 - w)) / max(na, 1e-4);
|
|
417
|
+
ovA = na;
|
|
418
|
+
}`;
|
|
419
|
+
}
|
|
420
|
+
return body;
|
|
421
|
+
},
|
|
422
|
+
uniforms: (node, dpr) => ({
|
|
423
|
+
radius: ["1f", (node.radius != null ? node.radius : 110) * dpr],
|
|
424
|
+
scale: ["1f", node.scale != null ? node.scale : 0.5],
|
|
425
|
+
rotate: ["1f", node.rotate != null ? node.rotate : 0],
|
|
426
|
+
fade: ["1f", node.fade != null ? node.fade : 1],
|
|
427
|
+
amount: ["1f", node.amount != null ? node.amount : 1],
|
|
428
|
+
}),
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
// Merge two sub-chains and blend the results.
|
|
432
|
+
//
|
|
433
|
+
// { op: "merge", mode: "screen", mix: 1.0,
|
|
434
|
+
// a: [ { op: "halftone" } ],
|
|
435
|
+
// b: [ { op: "duotone", colors: [...] } ] }
|
|
436
|
+
//
|
|
437
|
+
// Both branches start from the same state and neither sees the
|
|
438
|
+
// other's output — that is the difference between a merge and a
|
|
439
|
+
// chain. `mix` fades the whole merge back toward branch A, so
|
|
440
|
+
// `mix: 0` is A alone and `mix: 1` is the full blend.
|
|
441
|
+
//
|
|
442
|
+
// The shader work is emitted inline, so a merge costs no extra pass,
|
|
443
|
+
// no extra framebuffer and no second capture of the DOM. It cannot,
|
|
444
|
+
// for the same reason, give the branches different source content.
|
|
445
|
+
merge: {
|
|
446
|
+
// Never routed through the normal stage path — emitMerge()
|
|
447
|
+
// handles it — but it still needs decl/uniforms so its `mix`
|
|
448
|
+
// gets declared and uploaded like any other op's.
|
|
449
|
+
stage: "color",
|
|
450
|
+
decl: (p) => `uniform float ${p}mix;`,
|
|
451
|
+
code: () => "",
|
|
452
|
+
uniforms: (node) => ({ mix: ["1f", node.mix != null ? node.mix : 1] }),
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// Echo: temporal accumulation. Each frame is blended into a
|
|
456
|
+
// persistent full-resolution buffer, so content that CHANGES leaves a
|
|
457
|
+
// fading trail behind it — typing smears, a counter ticking over
|
|
458
|
+
// leaves a comet, a scrolling list ghosts.
|
|
459
|
+
//
|
|
460
|
+
// This is the one op that cannot work on the snapshot backend. A
|
|
461
|
+
// snapshot is a single frozen image, so accumulating it converges to
|
|
462
|
+
// that same still frame and nothing moves; it needs the live
|
|
463
|
+
// HTML-in-Canvas capture, where the texture is re-uploaded from the
|
|
464
|
+
// real DOM on every paint. Run it with the origin trial off and you
|
|
465
|
+
// correctly see nothing happen.
|
|
466
|
+
//
|
|
467
|
+
// { op: "echo", decay: 0.92, strength: 0.85 }
|
|
468
|
+
//
|
|
469
|
+
// The trail is written into the BORDER layer, which the frame
|
|
470
|
+
// skeleton composites underneath the content, so the ghosts sit
|
|
471
|
+
// behind the live text rather than veiling it.
|
|
472
|
+
echo: {
|
|
473
|
+
stage: "color",
|
|
474
|
+
decl: (p) => `
|
|
475
|
+
uniform sampler2D ${p}hist;
|
|
476
|
+
uniform float ${p}strength;
|
|
477
|
+
uniform vec3 ${p}tint;
|
|
478
|
+
uniform float ${p}tintAmt;`,
|
|
479
|
+
code: (p) => `
|
|
480
|
+
{
|
|
481
|
+
// Sampled in screen space: the trail records where things
|
|
482
|
+
// WERE, which is not where a warp op would fetch them from.
|
|
483
|
+
vec2 huv = vec2(frag.x / u_res.x, 1.0 - frag.y / u_res.y);
|
|
484
|
+
vec4 h = texture2D(${p}hist, clamp(huv, 0.001, 0.999));
|
|
485
|
+
float ha = clamp(h.a * ${p}strength, 0.0, 1.0);
|
|
486
|
+
vec3 hc = mix(h.rgb, ${p}tint, ${p}tintAmt);
|
|
487
|
+
// Straight-alpha OVER into the border layer so an edges op
|
|
488
|
+
// in the same chain still composites correctly.
|
|
489
|
+
float na = ha + edgeCov * (1.0 - ha);
|
|
490
|
+
edgeCol = (hc * ha + edgeCol * edgeCov * (1.0 - ha)) / max(na, 1e-4);
|
|
491
|
+
edgeCov = na;
|
|
492
|
+
}`,
|
|
493
|
+
uniforms: (node) => ({
|
|
494
|
+
strength: ["1f", node.strength != null ? node.strength : 0.85],
|
|
495
|
+
tint: ["3fv", hexToRgb(node.tint || "#7FD4FF")],
|
|
496
|
+
tintAmt: ["1f", node.tint ? (node.tintAmount != null ? node.tintAmount : 0.5) : 0.0],
|
|
497
|
+
}),
|
|
498
|
+
solver: {
|
|
499
|
+
// Full canvas resolution: a trail wants to be as sharp as the
|
|
500
|
+
// content it is trailing, not a coarse simulation grid.
|
|
501
|
+
resolutions: (node, w, h) => ({ frame: [w, h] }),
|
|
502
|
+
targets: { history: { double: true, res: "frame", fmt: "rgba", smooth: true } },
|
|
503
|
+
samplers: { hist: "history" },
|
|
504
|
+
programs: {
|
|
505
|
+
// out = max(content, history * decay)
|
|
506
|
+
//
|
|
507
|
+
// max rather than a mix: the brightest thing that has
|
|
508
|
+
// passed through a pixel is what persists, so trails read
|
|
509
|
+
// as light left behind instead of a muddy average. decay
|
|
510
|
+
// is per-frame and normalised for frame rate.
|
|
511
|
+
accumulate: `
|
|
512
|
+
precision highp float;
|
|
513
|
+
uniform sampler2D u_content;
|
|
514
|
+
uniform sampler2D u_hist;
|
|
515
|
+
uniform vec2 u_texel;
|
|
516
|
+
uniform float u_decay;
|
|
517
|
+
void main() {
|
|
518
|
+
vec2 uv = gl_FragCoord.xy * u_texel;
|
|
519
|
+
vec4 c = texture2D(u_content, uv);
|
|
520
|
+
vec4 h = texture2D(u_hist, uv) * u_decay;
|
|
521
|
+
gl_FragColor = max(c, h);
|
|
522
|
+
}`,
|
|
523
|
+
},
|
|
524
|
+
// echo takes no pointer input, so nothing to splat.
|
|
525
|
+
splatColor: () => [0, 0, 0],
|
|
526
|
+
step: (S, node, ctx) => {
|
|
527
|
+
const dt = Math.min(ctx.dt, 1 / 30);
|
|
528
|
+
const decay = Math.pow(node.decay != null ? node.decay : 0.92, dt * 60);
|
|
529
|
+
S.use("accumulate");
|
|
530
|
+
S.f2("u_texel", 1 / S.canvasW, 1 / S.canvasH);
|
|
531
|
+
S.f1("u_decay", decay);
|
|
532
|
+
S.smp("u_content", S.content, 0);
|
|
533
|
+
S.smp("u_hist", S.read("history"), 1);
|
|
534
|
+
S.blit(S.write("history"));
|
|
535
|
+
S.swap("history");
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
|
|
262
540
|
// Stirred liquid: an incompressible fluid whose velocity field warps
|
|
263
541
|
// the content and whose dye field colours it.
|
|
264
542
|
//
|
|
@@ -864,7 +1142,11 @@ function hexToRgb(c) {
|
|
|
864
1142
|
// Names designer.js uses to route nodes into this pipeline instead of
|
|
865
1143
|
// the CSS-level op loop. Kept as a live array so registerRasterOp()
|
|
866
1144
|
// extends routing too.
|
|
867
|
-
|
|
1145
|
+
// `switch` has no REGISTRY entry — it is resolved to a node list before
|
|
1146
|
+
// the shader is built — but it still has to be routed to elements like
|
|
1147
|
+
// any other raster node, so it is named here explicitly. Only the switch
|
|
1148
|
+
// node carries `target:`; the sub-chains inherit whatever it matched.
|
|
1149
|
+
const RASTER_OP_NAMES = Object.keys(REGISTRY).concat("switch");
|
|
868
1150
|
|
|
869
1151
|
// Extension surface — mirrors the CSS-level operation registry:
|
|
870
1152
|
// registerRasterOp("pixelate", { stage, decl, code, uniforms }).
|
|
@@ -882,25 +1164,165 @@ function isHTMLInCanvasAvailable() {
|
|
|
882
1164
|
|
|
883
1165
|
const VS = `attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }`;
|
|
884
1166
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1167
|
+
// What each stage is allowed to modify. Masking works by snapshotting
|
|
1168
|
+
// these before an op runs and lerping back toward the snapshot by the
|
|
1169
|
+
// mask value, so ANY op can be masked without the op knowing about it.
|
|
1170
|
+
const STAGE_VARS = {
|
|
1171
|
+
warp: ["warped"],
|
|
1172
|
+
cell: ["center", "edge"],
|
|
1173
|
+
displace: ["sampleP", "chroma"],
|
|
1174
|
+
color: ["col", "edgeCol", "edgeCov", "ovCol", "ovA"],
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// A node opts into a field with `masked: true` (the default field) or
|
|
1178
|
+
// `masked: "name"`. Field producers declare `as:` to name what they
|
|
1179
|
+
// write. This is the whole of the node-to-node data flow: one op writes
|
|
1180
|
+
// a scalar field, later ops read it.
|
|
1181
|
+
// No double underscores anywhere: GLSL reserves identifiers
|
|
1182
|
+
// containing `__` as possible future keywords, and Chrome rejects
|
|
1183
|
+
// the shader outright rather than warning.
|
|
1184
|
+
const fieldVar = (name) => `fld_${String(name).replace(/[^A-Za-z0-9_]/g, "")}`;
|
|
1185
|
+
const maskedField = (node) =>
|
|
1186
|
+
node.masked === true ? "mask" : (typeof node.masked === "string" ? node.masked : null);
|
|
1187
|
+
|
|
1188
|
+
const glslType = (v) =>
|
|
1189
|
+
(v === "edge" || v === "edgeCov" || v === "ovA") ? "float"
|
|
1190
|
+
: (v === "col" || v === "edgeCol" || v === "ovCol") ? "vec3"
|
|
1191
|
+
: "vec2";
|
|
1192
|
+
|
|
1193
|
+
// How `merge` combines its two branches. Applied to the vec3 stage
|
|
1194
|
+
// variables only; the scalars and coordinates are plainly interpolated,
|
|
1195
|
+
// because "screen" is meaningless for a displacement.
|
|
1196
|
+
const BLEND = {
|
|
1197
|
+
over: (a, b) => b,
|
|
1198
|
+
add: (a, b) => `min(${a} + ${b}, vec3(1.0))`,
|
|
1199
|
+
screen: (a, b) => `1.0 - (1.0 - ${a}) * (1.0 - ${b})`,
|
|
1200
|
+
multiply: (a, b) => `${a} * ${b}`,
|
|
1201
|
+
difference: (a, b) => `abs(${a} - ${b})`,
|
|
1202
|
+
lighten: (a, b) => `max(${a}, ${b})`,
|
|
1203
|
+
darken: (a, b) => `min(${a}, ${b})`,
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
const emptyBuckets = () =>
|
|
1207
|
+
({ field: "", warp: "", cell: "", displace: "", color: "" });
|
|
1208
|
+
|
|
1209
|
+
// Flatten a node tree (merge nodes carry sub-chains) into the linear
|
|
1210
|
+
// list that owns the uniform slots. Depth-first, parent before children,
|
|
1211
|
+
// so `u<i>_` prefixes are stable and every consumer — shader emission,
|
|
1212
|
+
// uniform upload, drivers, solvers — agrees on which node is index i.
|
|
1213
|
+
// Returns records rather than mutating the caller's node objects.
|
|
1214
|
+
function flattenRaster(nodes, flat) {
|
|
1215
|
+
const recs = [];
|
|
1216
|
+
for (const node of (nodes || [])) {
|
|
1217
|
+
if (!node || !node.op) continue;
|
|
1218
|
+
const rec = { node: node, i: flat.length };
|
|
1219
|
+
flat.push(node);
|
|
1220
|
+
if (node.op === "merge") {
|
|
1221
|
+
rec.a = flattenRaster(node.a, flat);
|
|
1222
|
+
rec.b = flattenRaster(node.b, flat);
|
|
1223
|
+
}
|
|
1224
|
+
recs.push(rec);
|
|
1225
|
+
}
|
|
1226
|
+
return recs;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function emitStages(recs, buckets) {
|
|
1230
|
+
for (const rec of recs) {
|
|
1231
|
+
const node = rec.node;
|
|
1232
|
+
if (node.op === "merge") { emitMerge(rec, buckets); continue; }
|
|
888
1233
|
const def = REGISTRY[node.op];
|
|
889
|
-
if (!def)
|
|
890
|
-
const p = `u${i}_`;
|
|
891
|
-
decls += def.decl(p, node) + "\n";
|
|
1234
|
+
if (!def) continue;
|
|
1235
|
+
const p = `u${rec.i}_`;
|
|
892
1236
|
// An op may contribute to more than one stage (stage: ["warp",
|
|
893
1237
|
// "color"]) — code() then receives the stage it is emitting for.
|
|
894
1238
|
// A plain string stage keeps the original single-snippet form.
|
|
895
1239
|
const stages = Array.isArray(def.stage) ? def.stage : [def.stage];
|
|
1240
|
+
const mask = maskedField(node);
|
|
896
1241
|
stages.forEach((st) => {
|
|
897
|
-
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1242
|
+
let snippet = def.code(p, node, st) + "\n";
|
|
1243
|
+
if (mask && STAGE_VARS[st]) {
|
|
1244
|
+
// Generic masking wrapper: no op needs to know it is
|
|
1245
|
+
// being masked, which is what lets a field constrain
|
|
1246
|
+
// ops written long before fields existed.
|
|
1247
|
+
const vars = STAGE_VARS[st];
|
|
1248
|
+
const snap = vars.map((v, k) =>
|
|
1249
|
+
` ${glslType(v)} msv${k} = ${v};`).join("\n");
|
|
1250
|
+
const back = vars.map((v, k) =>
|
|
1251
|
+
` ${v} = mix(msv${k}, ${v}, ${fieldVar(mask)});`).join("\n");
|
|
1252
|
+
snippet = ` {\n${snap}\n${snippet}\n${back}\n }\n`;
|
|
1253
|
+
}
|
|
1254
|
+
// `field` has no STAGE_VARS entry (it writes named fields,
|
|
1255
|
+
// not stage variables), so it is listed explicitly here —
|
|
1256
|
+
// anything unrecognised still falls through to colour.
|
|
1257
|
+
const bucket = (st === "field" || st === "warp" ||
|
|
1258
|
+
st === "cell" || st === "displace") ? st : "color";
|
|
1259
|
+
buckets[bucket] += snippet;
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// merge: run two sub-chains from the same starting state and combine
|
|
1265
|
+
// them. Still one pass — each branch's stage code is emitted into its
|
|
1266
|
+
// own scope, the stage variables are rewound between branches, and the
|
|
1267
|
+
// two results are blended. That is what makes it free: no second
|
|
1268
|
+
// framebuffer, no second capture of the DOM.
|
|
1269
|
+
//
|
|
1270
|
+
// The consequence is that a branch cannot see the other's output, only
|
|
1271
|
+
// the shared input, which is exactly the semantics you want for a merge
|
|
1272
|
+
// and not the semantics of a chain.
|
|
1273
|
+
function emitMerge(rec, buckets) {
|
|
1274
|
+
const p = `u${rec.i}_`;
|
|
1275
|
+
const blend = BLEND[rec.node.mode] || BLEND.over;
|
|
1276
|
+
const A = emptyBuckets(), B = emptyBuckets();
|
|
1277
|
+
emitStages(rec.a || [], A);
|
|
1278
|
+
emitStages(rec.b || [], B);
|
|
1279
|
+
// Fields are shared state, not branch-local: a mask written inside
|
|
1280
|
+
// one branch is visible to the other, and to everything after.
|
|
1281
|
+
buckets.field += A.field + B.field;
|
|
1282
|
+
for (const st of ["warp", "cell", "displace", "color"]) {
|
|
1283
|
+
if (!A[st] && !B[st]) continue;
|
|
1284
|
+
const vars = STAGE_VARS[st];
|
|
1285
|
+
let s = " {\n";
|
|
1286
|
+
vars.forEach((v, k) => { s += ` ${glslType(v)} pre${k} = ${v};\n`; });
|
|
1287
|
+
s += A[st];
|
|
1288
|
+
vars.forEach((v, k) => { s += ` ${glslType(v)} bra${k} = ${v};\n`; });
|
|
1289
|
+
vars.forEach((v, k) => { s += ` ${v} = pre${k};\n`; });
|
|
1290
|
+
s += B[st];
|
|
1291
|
+
vars.forEach((v, k) => {
|
|
1292
|
+
const combined = glslType(v) === "vec3"
|
|
1293
|
+
? blend(`bra${k}`, v)
|
|
1294
|
+
: v;
|
|
1295
|
+
s += ` ${v} = mix(bra${k}, ${combined}, ${p}mix);\n`;
|
|
902
1296
|
});
|
|
1297
|
+
s += " }\n";
|
|
1298
|
+
buckets[st] += s;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function buildFragmentShader(recs, flat) {
|
|
1303
|
+
let decls = "";
|
|
1304
|
+
|
|
1305
|
+
// Every field any node writes or reads, declared up front at 1.0 so
|
|
1306
|
+
// an unmatched reference is a no-op rather than a compile error.
|
|
1307
|
+
const fieldNames = new Set();
|
|
1308
|
+
flat.forEach((n) => {
|
|
1309
|
+
const def = REGISTRY[n.op];
|
|
1310
|
+
if (def && def.producesField) fieldNames.add(n.as || "mask");
|
|
1311
|
+
const m = maskedField(n);
|
|
1312
|
+
if (m) fieldNames.add(m);
|
|
1313
|
+
});
|
|
1314
|
+
const fieldDecls = [...fieldNames]
|
|
1315
|
+
.map((n) => ` float ${fieldVar(n)} = 1.0;`).join("\n");
|
|
1316
|
+
|
|
1317
|
+
flat.forEach((node, i) => {
|
|
1318
|
+
const def = REGISTRY[node.op];
|
|
1319
|
+
if (def) decls += def.decl(`u${i}_`, node) + "\n";
|
|
903
1320
|
});
|
|
1321
|
+
|
|
1322
|
+
const buckets = emptyBuckets();
|
|
1323
|
+
emitStages(recs, buckets);
|
|
1324
|
+
const { field, warp, cell, displace, color } = buckets;
|
|
1325
|
+
const nodes = flat;
|
|
904
1326
|
// Compositing, in three layers (all straight-alpha):
|
|
905
1327
|
// 1. border layer (edgeCol/edgeCov, from an edges op) UNDER
|
|
906
1328
|
// 2. content layer (col over tex.a) then
|
|
@@ -926,6 +1348,8 @@ uniform float u_time;
|
|
|
926
1348
|
${decls}
|
|
927
1349
|
void main() {
|
|
928
1350
|
vec2 frag = vec2(gl_FragCoord.x, u_res.y - gl_FragCoord.y);
|
|
1351
|
+
${fieldDecls}
|
|
1352
|
+
${field}
|
|
929
1353
|
vec2 warped = frag;
|
|
930
1354
|
${warp}
|
|
931
1355
|
vec2 center = warped;
|
|
@@ -999,6 +1423,59 @@ function snapshotToImage(el, w, h, dpr) {
|
|
|
999
1423
|
});
|
|
1000
1424
|
}
|
|
1001
1425
|
|
|
1426
|
+
// ── switch: pick a sub-chain by condition ────────────────────────────
|
|
1427
|
+
//
|
|
1428
|
+
// { op: "switch",
|
|
1429
|
+
// when: "(max-width: 700px)", // or "coarse", "fine", "no-hover"
|
|
1430
|
+
// use: [ { op: "halftone" } ], // taken when the test passes
|
|
1431
|
+
// else: [ { op: "stir" }, ... ] } // taken when it does not
|
|
1432
|
+
//
|
|
1433
|
+
// This is a build-time node, not a shader op: it produces a node list,
|
|
1434
|
+
// which is then compiled like any other. Sub-chains nest, so a switch
|
|
1435
|
+
// may contain a switch.
|
|
1436
|
+
//
|
|
1437
|
+
// Evaluated ONCE, when the pipeline attaches. It is meant for choices
|
|
1438
|
+
// that are properties of the device — a cheap chain on a phone, a heavy
|
|
1439
|
+
// one on a desktop — not for anything that should change as the window
|
|
1440
|
+
// is dragged: switching mid-session would need a new shader program,
|
|
1441
|
+
// and the pipeline compiles exactly one.
|
|
1442
|
+
const SWITCH_ALIASES = {
|
|
1443
|
+
coarse: "(pointer: coarse)",
|
|
1444
|
+
fine: "(pointer: fine)",
|
|
1445
|
+
hover: "(hover: hover)",
|
|
1446
|
+
"no-hover": "(hover: none)",
|
|
1447
|
+
"reduced-motion": "(prefers-reduced-motion: reduce)",
|
|
1448
|
+
dark: "(prefers-color-scheme: dark)",
|
|
1449
|
+
light: "(prefers-color-scheme: light)",
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
function switchTest(when) {
|
|
1453
|
+
if (typeof when === "function") {
|
|
1454
|
+
try { return !!when(); } catch (e) { return false; }
|
|
1455
|
+
}
|
|
1456
|
+
if (typeof when === "boolean") return when;
|
|
1457
|
+
if (typeof when !== "string" || when === "") return false;
|
|
1458
|
+
const q = SWITCH_ALIASES[when] || when;
|
|
1459
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
1460
|
+
try { return window.matchMedia(q).matches; } catch (e) { return false; }
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function resolveSwitches(nodes, depth) {
|
|
1464
|
+
if (!Array.isArray(nodes)) return [];
|
|
1465
|
+
// Cheap cycle guard: sub-chains are plain data and could be shared
|
|
1466
|
+
// by reference, so a self-referential list would otherwise hang.
|
|
1467
|
+
if ((depth || 0) > 8) return [];
|
|
1468
|
+
const out = [];
|
|
1469
|
+
for (const node of nodes) {
|
|
1470
|
+
if (!node || node.op !== "switch") { out.push(node); continue; }
|
|
1471
|
+
const branch = switchTest(node.when) ? node.use : node.else;
|
|
1472
|
+
// A branch may legitimately be empty — that is how you say
|
|
1473
|
+
// "do nothing on small screens".
|
|
1474
|
+
for (const sub of resolveSwitches(branch || [], (depth || 0) + 1)) out.push(sub);
|
|
1475
|
+
}
|
|
1476
|
+
return out;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1002
1479
|
// ── Pipeline runner ──────────────────────────────────────────────────
|
|
1003
1480
|
|
|
1004
1481
|
function applyRasterPipeline(el, rasterNodes) {
|
|
@@ -1007,6 +1484,20 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1007
1484
|
// page untouched.
|
|
1008
1485
|
if (!el || typeof document === "undefined" || typeof window === "undefined") return null;
|
|
1009
1486
|
if (!rasterNodes || rasterNodes.length === 0) return null;
|
|
1487
|
+
// Flatten any switch nodes down to the chain this device actually
|
|
1488
|
+
// gets, before anything reads the list.
|
|
1489
|
+
if (rasterNodes.some((n) => n && n.op === "switch")) {
|
|
1490
|
+
rasterNodes = resolveSwitches(rasterNodes, 0);
|
|
1491
|
+
if (rasterNodes.length === 0) return null;
|
|
1492
|
+
}
|
|
1493
|
+
// Flatten merge sub-chains into the linear list that owns the
|
|
1494
|
+
// uniform slots. `tree` keeps the branch structure for the shader
|
|
1495
|
+
// emitter; `flat` is what every other loop iterates, so a node
|
|
1496
|
+
// nested inside a merge gets its uniforms, driver and solver set up
|
|
1497
|
+
// exactly like a top-level one.
|
|
1498
|
+
const flatNodes = [];
|
|
1499
|
+
const rasterTree = flattenRaster(rasterNodes, flatNodes);
|
|
1500
|
+
rasterNodes = flatNodes;
|
|
1010
1501
|
if (typeof window.matchMedia === "function" &&
|
|
1011
1502
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
|
|
1012
1503
|
|
|
@@ -1147,7 +1638,7 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1147
1638
|
};
|
|
1148
1639
|
const prog = gl.createProgram();
|
|
1149
1640
|
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
|
|
1150
|
-
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, buildFragmentShader(rasterNodes)));
|
|
1641
|
+
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, buildFragmentShader(rasterTree, rasterNodes)));
|
|
1151
1642
|
// Pin "a" to location 0 so a field-simulation program (which shares
|
|
1152
1643
|
// this vertex buffer and attribute array) can be swapped in without
|
|
1153
1644
|
// respecifying the pointer.
|
|
@@ -1206,7 +1697,9 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1206
1697
|
return null;
|
|
1207
1698
|
}
|
|
1208
1699
|
gl.getExtension("OES_texture_float_linear");
|
|
1209
|
-
|
|
1700
|
+
// Canvas dimensions are passed through so a target can be
|
|
1701
|
+
// full-resolution (echo's history) rather than a fixed grid.
|
|
1702
|
+
const res = def.solver.resolutions(node, canvas.width, canvas.height);
|
|
1210
1703
|
const FMT = {
|
|
1211
1704
|
r: [gl.R16F, gl.RED],
|
|
1212
1705
|
rg: [gl.RG16F, gl.RG],
|
|
@@ -1214,7 +1707,7 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1214
1707
|
};
|
|
1215
1708
|
|
|
1216
1709
|
const made = [];
|
|
1217
|
-
const mkTarget = (
|
|
1710
|
+
const mkTarget = (w, h, fmt, smooth) => {
|
|
1218
1711
|
const [internal, format] = FMT[fmt];
|
|
1219
1712
|
const t = gl.createTexture();
|
|
1220
1713
|
gl.bindTexture(gl.TEXTURE_2D, t);
|
|
@@ -1223,17 +1716,17 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1223
1716
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, flt);
|
|
1224
1717
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1225
1718
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1226
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, internal,
|
|
1719
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, internal, w, h, 0,
|
|
1227
1720
|
format, gl.HALF_FLOAT, null);
|
|
1228
1721
|
const fbo = gl.createFramebuffer();
|
|
1229
1722
|
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1230
1723
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
|
|
1231
1724
|
gl.TEXTURE_2D, t, 0);
|
|
1232
1725
|
const ok = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
1233
|
-
gl.viewport(0, 0,
|
|
1726
|
+
gl.viewport(0, 0, w, h);
|
|
1234
1727
|
gl.clearColor(0, 0, 0, 1);
|
|
1235
1728
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1236
|
-
const rec = { tex: t, fbo,
|
|
1729
|
+
const rec = { tex: t, fbo, w, h, ok };
|
|
1237
1730
|
made.push(rec);
|
|
1238
1731
|
return rec;
|
|
1239
1732
|
};
|
|
@@ -1242,15 +1735,16 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1242
1735
|
let allOk = true;
|
|
1243
1736
|
for (const name in def.solver.targets) {
|
|
1244
1737
|
const spec = def.solver.targets[name];
|
|
1245
|
-
const
|
|
1738
|
+
const dim = res[spec.res];
|
|
1739
|
+
const [tw, th] = Array.isArray(dim) ? dim : [dim, dim];
|
|
1246
1740
|
if (spec.double) {
|
|
1247
1741
|
targets[name] = {
|
|
1248
|
-
a: mkTarget(
|
|
1249
|
-
b: mkTarget(
|
|
1742
|
+
a: mkTarget(tw, th, spec.fmt, spec.smooth),
|
|
1743
|
+
b: mkTarget(tw, th, spec.fmt, spec.smooth),
|
|
1250
1744
|
};
|
|
1251
1745
|
allOk = allOk && targets[name].a.ok && targets[name].b.ok;
|
|
1252
1746
|
} else {
|
|
1253
|
-
targets[name] = { a: mkTarget(
|
|
1747
|
+
targets[name] = { a: mkTarget(tw, th, spec.fmt, spec.smooth) };
|
|
1254
1748
|
allOk = allOk && targets[name].a.ok;
|
|
1255
1749
|
}
|
|
1256
1750
|
}
|
|
@@ -1325,13 +1819,18 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1325
1819
|
read: (name) => s.targets[name].a,
|
|
1326
1820
|
write: (name) => s.targets[name].b,
|
|
1327
1821
|
single: (name) => s.targets[name].a,
|
|
1822
|
+
// The captured page content, as a bindable target. echo
|
|
1823
|
+
// accumulates from it; stir never touches it.
|
|
1824
|
+
content: { tex },
|
|
1825
|
+
canvasW: canvas.width,
|
|
1826
|
+
canvasH: canvas.height,
|
|
1328
1827
|
swap(name) {
|
|
1329
1828
|
const t = s.targets[name];
|
|
1330
1829
|
const tmp = t.a; t.a = t.b; t.b = tmp;
|
|
1331
1830
|
},
|
|
1332
1831
|
blit(target) {
|
|
1333
1832
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
|
|
1334
|
-
gl.viewport(0, 0, target.
|
|
1833
|
+
gl.viewport(0, 0, target.w, target.h);
|
|
1335
1834
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1336
1835
|
},
|
|
1337
1836
|
};
|
|
@@ -1689,20 +2188,25 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1689
2188
|
|
|
1690
2189
|
let ro = null;
|
|
1691
2190
|
let resizeTimer = 0;
|
|
2191
|
+
// Re-measure the host and capture again. Shared by the
|
|
2192
|
+
// ResizeObserver, the window-resize fallback and the font-loading
|
|
2193
|
+
// recapture, so all three agree on what "resize the canvas" means.
|
|
2194
|
+
const remeasureAndCapture = () => {
|
|
2195
|
+
if (destroyed || mode !== "snapshot") return;
|
|
2196
|
+
const m = measure();
|
|
2197
|
+
rect.width = m.width;
|
|
2198
|
+
rect.height = m.height;
|
|
2199
|
+
canvas.width = Math.round(m.width * dpr);
|
|
2200
|
+
canvas.height = Math.round(m.height * dpr);
|
|
2201
|
+
canvas.style.width = m.width + "px";
|
|
2202
|
+
canvas.style.height = m.height + "px";
|
|
2203
|
+
snapshotCapture();
|
|
2204
|
+
};
|
|
2205
|
+
|
|
1692
2206
|
if (typeof ResizeObserver !== "undefined") {
|
|
1693
2207
|
ro = new ResizeObserver(() => {
|
|
1694
2208
|
clearTimeout(resizeTimer);
|
|
1695
|
-
resizeTimer = setTimeout(
|
|
1696
|
-
if (destroyed || mode !== "snapshot") return;
|
|
1697
|
-
const m = measure();
|
|
1698
|
-
rect.width = m.width;
|
|
1699
|
-
rect.height = m.height;
|
|
1700
|
-
canvas.width = Math.round(m.width * dpr);
|
|
1701
|
-
canvas.height = Math.round(m.height * dpr);
|
|
1702
|
-
canvas.style.width = m.width + "px";
|
|
1703
|
-
canvas.style.height = m.height + "px";
|
|
1704
|
-
snapshotCapture();
|
|
1705
|
-
}, 150);
|
|
2209
|
+
resizeTimer = setTimeout(remeasureAndCapture, 150);
|
|
1706
2210
|
});
|
|
1707
2211
|
ro.observe(el);
|
|
1708
2212
|
}
|
|
@@ -1719,17 +2223,7 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1719
2223
|
clearTimeout(winResizeTimer);
|
|
1720
2224
|
winResizeTimer = setTimeout(() => {
|
|
1721
2225
|
if (destroyed) return;
|
|
1722
|
-
if (mode === "snapshot") {
|
|
1723
|
-
const m = measure();
|
|
1724
|
-
rect.width = m.width;
|
|
1725
|
-
rect.height = m.height;
|
|
1726
|
-
canvas.width = Math.round(m.width * dpr);
|
|
1727
|
-
canvas.height = Math.round(m.height * dpr);
|
|
1728
|
-
canvas.style.width = m.width + "px";
|
|
1729
|
-
canvas.style.height = m.height + "px";
|
|
1730
|
-
snapshotCapture();
|
|
1731
|
-
return;
|
|
1732
|
-
}
|
|
2226
|
+
if (mode === "snapshot") { remeasureAndCapture(); return; }
|
|
1733
2227
|
const w = el.clientWidth ||
|
|
1734
2228
|
(el.parentElement && el.parentElement.clientWidth) || rect.width;
|
|
1735
2229
|
canvas.style.width = w + "px";
|
|
@@ -1754,6 +2248,19 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1754
2248
|
raf = requestAnimationFrame(draw);
|
|
1755
2249
|
} else {
|
|
1756
2250
|
snapshotCapture().then(() => { raf = requestAnimationFrame(draw); });
|
|
2251
|
+
// A web font that arrives after the first capture changes glyph
|
|
2252
|
+
// metrics, and therefore line breaking: the snapshot would keep
|
|
2253
|
+
// whatever the fallback font wrapped to while the DOM underneath
|
|
2254
|
+
// reflows to something else. The element box does not necessarily
|
|
2255
|
+
// change, so the ResizeObserver never fires and the mismatch
|
|
2256
|
+
// persists until an unrelated resize. Capture once more when the
|
|
2257
|
+
// fonts settle.
|
|
2258
|
+
if (typeof document !== "undefined" && document.fonts &&
|
|
2259
|
+
typeof document.fonts.ready === "object") {
|
|
2260
|
+
Promise.resolve(document.fonts.ready)
|
|
2261
|
+
.then(remeasureAndCapture)
|
|
2262
|
+
.catch(() => {});
|
|
2263
|
+
}
|
|
1757
2264
|
}
|
|
1758
2265
|
|
|
1759
2266
|
return {
|
|
@@ -1788,4 +2295,7 @@ function applyRasterPipeline(el, rasterNodes) {
|
|
|
1788
2295
|
export {
|
|
1789
2296
|
applyRasterPipeline, registerRasterOp, RASTER_OP_NAMES,
|
|
1790
2297
|
isHTMLInCanvasAvailable, DRIVER_NAMES,
|
|
2298
|
+
// Exported so which branch a `switch` takes can be asserted directly
|
|
2299
|
+
// rather than inferred from pixels.
|
|
2300
|
+
resolveSwitches,
|
|
1791
2301
|
};
|