nodality 1.0.177 → 1.0.180
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 +34 -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 -3
- 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 -3
- 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 +5 -2
- 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 +17 -2
- package/lib/element-mapper.js +24 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/raster-ops.js +593 -0
- 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
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.0.180
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// raster-ops.js — Houdini-like raster operation pipeline for Nodality.
|
|
8
|
+
//
|
|
9
|
+
// A second op category next to the CSS-level ops in designer.js
|
|
10
|
+
// ("blast", "gradient", ...): raster ops compile the *rendered page*
|
|
11
|
+
// into a WebGL texture and run an ordered chain of GPU stages over it,
|
|
12
|
+
// driven by the same plain-data `nodes` syntax:
|
|
13
|
+
//
|
|
14
|
+
// let nodes = [
|
|
15
|
+
// { op: "hexalize", size: 26 },
|
|
16
|
+
// { op: "offset", by: "mouse", strength: 22, radius: 280 },
|
|
17
|
+
// { op: "duotone", by: "mouse", colors: ["#104B87", "#E8FF00"], radius: 200 },
|
|
18
|
+
// ];
|
|
19
|
+
//
|
|
20
|
+
// Texture backends (capture of the live DOM):
|
|
21
|
+
// snapshot (default) — XMLSerializer -> SVG foreignObject -> Image ->
|
|
22
|
+
// texture. Works in every browser. Nodality output is inline-styled,
|
|
23
|
+
// so fidelity is high. Static: recaptured on resize (or via
|
|
24
|
+
// handle.refresh()). External images / webfonts do not load inside
|
|
25
|
+
// the SVG image context — system fonts and data: URIs only.
|
|
26
|
+
// live (opt-in: any raster node carries `live: true`) — the emerging
|
|
27
|
+
// HTML-in-Canvas API (WICG, Chrome origin trial): the mount subtree
|
|
28
|
+
// moves inside the effect <canvas layoutsubtree> and is uploaded per
|
|
29
|
+
// frame with gl.texElementImage2D(), staying interactive and live.
|
|
30
|
+
// Falls back to snapshot when the API is absent.
|
|
31
|
+
//
|
|
32
|
+
// Safety: this module touches nothing at import time, so it is inert
|
|
33
|
+
// under jsdom prerender (nodality/ssg). applyRasterPipeline() returns
|
|
34
|
+
// null (never throws) when WebGL / DOM / motion are unavailable.
|
|
35
|
+
|
|
36
|
+
// ── Op registry ──────────────────────────────────────────────────────
|
|
37
|
+
// Each op contributes GLSL to one stage of a fixed frame skeleton:
|
|
38
|
+
// warp — displace the coordinate space itself (warped) e.g. offset;
|
|
39
|
+
// runs BEFORE the cell stage so grids/edges move WITH the
|
|
40
|
+
// content instead of staying a fixed screen-space lattice
|
|
41
|
+
// cell — redefine the sampling cell (center, edge) e.g. hexalize
|
|
42
|
+
// displace — move only the sample position (sampleP)
|
|
43
|
+
// color — transform the sampled colour (col) e.g. duotone
|
|
44
|
+
// Ops execute in nodes-array order within their stage. Uniforms are
|
|
45
|
+
// namespaced per node index so the same op can appear twice.
|
|
46
|
+
|
|
47
|
+
const REGISTRY = {
|
|
48
|
+
hexalize: {
|
|
49
|
+
stage: "cell",
|
|
50
|
+
decl: (p) => `uniform float ${p}size;`,
|
|
51
|
+
code: (p) => `
|
|
52
|
+
{
|
|
53
|
+
vec2 hp = warped / ${p}size;
|
|
54
|
+
vec2 hr = vec2(1.0, 1.7320508), hh = hr * 0.5;
|
|
55
|
+
vec2 ha = mod(hp, hr) - hh, hb = mod(hp - hh, hr) - hh;
|
|
56
|
+
vec2 gv = dot(ha, ha) < dot(hb, hb) ? ha : hb;
|
|
57
|
+
center = (hp - gv) * ${p}size;
|
|
58
|
+
gv = abs(gv);
|
|
59
|
+
edge = max(edge, smoothstep(0.44, 0.5,
|
|
60
|
+
max(dot(gv, normalize(vec2(1.0, 1.7320508))), gv.x)));
|
|
61
|
+
}`,
|
|
62
|
+
uniforms: (node, dpr) => ({ size: ["1f", (node.size || 24) * dpr] }),
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
offset: {
|
|
66
|
+
// Warp-stage: displaces the coordinate space before the cell
|
|
67
|
+
// grid is computed, so tiles — content AND borders — move
|
|
68
|
+
// together away from the pointer. (The old displace-stage
|
|
69
|
+
// version shifted only the texture lookup, which left grid
|
|
70
|
+
// edges frozen in screen space.)
|
|
71
|
+
stage: "warp",
|
|
72
|
+
decl: (p) => `uniform float ${p}strength; uniform float ${p}radius;`,
|
|
73
|
+
code: (p) => `
|
|
74
|
+
{
|
|
75
|
+
float d = length(warped - u_mouse);
|
|
76
|
+
float fall = 1.0 - smoothstep(0.0, ${p}radius, d);
|
|
77
|
+
vec2 dir = d > 0.5 ? (warped - u_mouse) / d : vec2(0.0);
|
|
78
|
+
warped -= dir * ${p}strength * fall;
|
|
79
|
+
}`,
|
|
80
|
+
uniforms: (node, dpr) => ({
|
|
81
|
+
strength: ["1f", (node.strength || 20) * dpr],
|
|
82
|
+
radius: ["1f", (node.radius || 260) * dpr],
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
duotone: {
|
|
87
|
+
stage: "color",
|
|
88
|
+
decl: (p) => `uniform vec3 ${p}a; uniform vec3 ${p}b; uniform float ${p}radius;`,
|
|
89
|
+
code: (p, node) => `
|
|
90
|
+
{
|
|
91
|
+
float lum = dot(col, vec3(0.299, 0.587, 0.114));
|
|
92
|
+
vec3 duo = mix(${p}a, ${p}b, lum);
|
|
93
|
+
${node.by === "mouse"
|
|
94
|
+
? `float mask = 1.0 - smoothstep(${p}radius * 0.35, ${p}radius, length(center - u_mouse));`
|
|
95
|
+
: `float mask = 1.0;`}
|
|
96
|
+
col = mix(col, duo, mask);
|
|
97
|
+
}`,
|
|
98
|
+
uniforms: (node, dpr) => {
|
|
99
|
+
const cols = node.colors || ["#104B87", "#E8FF00"];
|
|
100
|
+
return {
|
|
101
|
+
a: ["3fv", hexToRgb(cols[0])],
|
|
102
|
+
b: ["3fv", hexToRgb(cols[1])],
|
|
103
|
+
radius: ["1f", (node.radius || 220) * dpr],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Styles the cell seams produced by the cell stage (hexalize et al)
|
|
109
|
+
// as a separate BORDER LAYER (edgeCol / edgeCov) rather than mixing
|
|
110
|
+
// into the content. The final composite draws the element's content
|
|
111
|
+
// (the text) OVER this layer, so borders sit below the glyphs:
|
|
112
|
+
// e.g. { op: "edges", color: "#FFFFFF" } gives white borders under
|
|
113
|
+
// the text. When present it also replaces the default seam darkening.
|
|
114
|
+
edges: {
|
|
115
|
+
stage: "color",
|
|
116
|
+
decl: (p) => `uniform vec3 ${p}color; uniform float ${p}strength;`,
|
|
117
|
+
code: (p) => `
|
|
118
|
+
{
|
|
119
|
+
edgeCol = ${p}color;
|
|
120
|
+
edgeCov = max(edgeCov, edge * ${p}strength);
|
|
121
|
+
}`,
|
|
122
|
+
uniforms: (node) => ({
|
|
123
|
+
color: ["3fv", hexToRgb(node.color || "#FFFFFF")],
|
|
124
|
+
strength: ["1f", node.strength != null ? node.strength : 1.0],
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function hexToRgb(c) {
|
|
130
|
+
return [1, 3, 5].map((i) => parseInt(c.slice(i, i + 2), 16) / 255);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Names designer.js uses to route nodes into this pipeline instead of
|
|
134
|
+
// the CSS-level op loop. Kept as a live array so registerRasterOp()
|
|
135
|
+
// extends routing too.
|
|
136
|
+
const RASTER_OP_NAMES = Object.keys(REGISTRY);
|
|
137
|
+
|
|
138
|
+
// Extension surface — mirrors the CSS-level operation registry:
|
|
139
|
+
// registerRasterOp("pixelate", { stage, decl, code, uniforms }).
|
|
140
|
+
function registerRasterOp(name, def) {
|
|
141
|
+
REGISTRY[name] = def;
|
|
142
|
+
if (!RASTER_OP_NAMES.includes(name)) RASTER_OP_NAMES.push(name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isHTMLInCanvasAvailable() {
|
|
146
|
+
if (typeof WebGLRenderingContext === "undefined") return false;
|
|
147
|
+
return "texElementImage2D" in WebGLRenderingContext.prototype;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Shader assembly ──────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const VS = `attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }`;
|
|
153
|
+
|
|
154
|
+
function buildFragmentShader(nodes) {
|
|
155
|
+
let decls = "", warp = "", cell = "", displace = "", color = "";
|
|
156
|
+
nodes.forEach((node, i) => {
|
|
157
|
+
const def = REGISTRY[node.op];
|
|
158
|
+
if (!def) return;
|
|
159
|
+
const p = `u${i}_`;
|
|
160
|
+
decls += def.decl(p) + "\n";
|
|
161
|
+
const snippet = def.code(p, node) + "\n";
|
|
162
|
+
if (def.stage === "warp") warp += snippet;
|
|
163
|
+
else if (def.stage === "cell") cell += snippet;
|
|
164
|
+
else if (def.stage === "displace") displace += snippet;
|
|
165
|
+
else color += snippet;
|
|
166
|
+
});
|
|
167
|
+
// When an edges op is present it emits a border layer (edgeCol /
|
|
168
|
+
// edgeCov) and the content (text) is composited OVER it, so borders
|
|
169
|
+
// sit below the glyphs. Otherwise cells get the default subtle
|
|
170
|
+
// darkening so the tiling still reads.
|
|
171
|
+
const hasEdges = nodes.some((n) => n.op === "edges");
|
|
172
|
+
const finalBlock = hasEdges
|
|
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);`;
|
|
179
|
+
return `
|
|
180
|
+
precision highp float;
|
|
181
|
+
uniform sampler2D u_tex;
|
|
182
|
+
uniform vec2 u_res;
|
|
183
|
+
uniform vec2 u_mouse;
|
|
184
|
+
${decls}
|
|
185
|
+
void main() {
|
|
186
|
+
vec2 frag = vec2(gl_FragCoord.x, u_res.y - gl_FragCoord.y);
|
|
187
|
+
vec2 warped = frag;
|
|
188
|
+
${warp}
|
|
189
|
+
vec2 center = warped;
|
|
190
|
+
float edge = 0.0;
|
|
191
|
+
${cell}
|
|
192
|
+
vec2 sampleP = warped;
|
|
193
|
+
${displace}
|
|
194
|
+
vec2 uv = vec2(sampleP.x / u_res.x, 1.0 - sampleP.y / u_res.y);
|
|
195
|
+
vec4 tex = texture2D(u_tex, clamp(uv, 0.001, 0.999));
|
|
196
|
+
vec3 col = tex.rgb;
|
|
197
|
+
vec3 edgeCol = vec3(1.0);
|
|
198
|
+
float edgeCov = 0.0;
|
|
199
|
+
${color}
|
|
200
|
+
${finalBlock}
|
|
201
|
+
}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Snapshot backend: DOM subtree -> SVG foreignObject -> texture ────
|
|
205
|
+
|
|
206
|
+
function snapshotToImage(el, w, h, dpr) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const serialized = new XMLSerializer().serializeToString(el);
|
|
209
|
+
const svg =
|
|
210
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${w * dpr}" height="${h * dpr}" viewBox="0 0 ${w} ${h}">` +
|
|
211
|
+
`<foreignObject width="100%" height="100%">` +
|
|
212
|
+
`<div xmlns="http://www.w3.org/1999/xhtml">${serialized}</div>` +
|
|
213
|
+
`</foreignObject></svg>`;
|
|
214
|
+
const img = new Image();
|
|
215
|
+
img.onload = () => resolve(img);
|
|
216
|
+
img.onerror = reject;
|
|
217
|
+
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Pipeline runner ──────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
function applyRasterPipeline(el, rasterNodes) {
|
|
224
|
+
// Hard guards — every early-out is silent by design so that jsdom
|
|
225
|
+
// prerender, old browsers and reduced-motion users get the plain
|
|
226
|
+
// page untouched.
|
|
227
|
+
if (!el || typeof document === "undefined" || typeof window === "undefined") return null;
|
|
228
|
+
if (!rasterNodes || rasterNodes.length === 0) return null;
|
|
229
|
+
if (typeof window.matchMedia === "function" &&
|
|
230
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
|
|
231
|
+
|
|
232
|
+
// One pipeline per host element (set() chains can fire more than
|
|
233
|
+
// once across codegen instances; only the mounted one matters).
|
|
234
|
+
if (el.querySelector && el.querySelector(":scope > canvas[data-nodality-raster]")) return null;
|
|
235
|
+
|
|
236
|
+
// Content-box measurement: some elements (fluid-sized Text) keep a
|
|
237
|
+
// collapsed layout box while their glyphs overflow it, so fall back
|
|
238
|
+
// to scroll dimensions when the rect is degenerate.
|
|
239
|
+
const measure = () => {
|
|
240
|
+
const r = el.getBoundingClientRect();
|
|
241
|
+
return {
|
|
242
|
+
width: Math.max(r.width, el.scrollWidth || 0),
|
|
243
|
+
height: Math.max(r.height, el.scrollHeight || 0),
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const rect = measure();
|
|
247
|
+
if (rect.width < 2 || rect.height < 2) return null;
|
|
248
|
+
|
|
249
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
250
|
+
const wantLive = rasterNodes.some((n) => n.live === true);
|
|
251
|
+
const apiAvailable =
|
|
252
|
+
(typeof WebGL2RenderingContext !== "undefined" &&
|
|
253
|
+
"texElementImage2D" in WebGL2RenderingContext.prototype) ||
|
|
254
|
+
(typeof WebGLRenderingContext !== "undefined" &&
|
|
255
|
+
"texElementImage2D" in WebGLRenderingContext.prototype);
|
|
256
|
+
let live = wantLive && apiAvailable;
|
|
257
|
+
if (wantLive && !live) {
|
|
258
|
+
console.info("[nodality] HTML-in-Canvas API not available - raster ops fall back to snapshot capture.");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const canvas = document.createElement("canvas");
|
|
262
|
+
// layoutsubtree must be present before context creation so the
|
|
263
|
+
// browser prepares the canvas for laid-out children (origin trial).
|
|
264
|
+
if (live) canvas.setAttribute("layoutsubtree", "");
|
|
265
|
+
const ctxOpts = { alpha: true, premultipliedAlpha: false };
|
|
266
|
+
// The origin trial wires texElementImage2D primarily on WebGL2 —
|
|
267
|
+
// prefer it in live mode, fall back to WebGL1 (same GLSL either way).
|
|
268
|
+
const gl = (live && canvas.getContext("webgl2", ctxOpts)) ||
|
|
269
|
+
canvas.getContext("webgl", ctxOpts);
|
|
270
|
+
if (!gl) return null;
|
|
271
|
+
if (live && !("texElementImage2D" in gl)) {
|
|
272
|
+
console.info("[nodality] texElementImage2D missing on the created context - snapshot fallback.");
|
|
273
|
+
canvas.removeAttribute("layoutsubtree");
|
|
274
|
+
live = false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Overlay the effect canvas on the host element. The real DOM stays
|
|
278
|
+
// in place underneath (accessibility tree, selection, focus), the
|
|
279
|
+
// canvas is purely visual and lets pointer events through.
|
|
280
|
+
const cs = window.getComputedStyle(el);
|
|
281
|
+
if (cs.position === "static") el.style.position = "relative";
|
|
282
|
+
canvas.width = Math.round(rect.width * dpr);
|
|
283
|
+
canvas.height = Math.round(rect.height * dpr);
|
|
284
|
+
canvas.setAttribute("data-nodality-raster", live ? "live" : "snapshot");
|
|
285
|
+
console.info("[nodality] raster backend:", live ? "html-in-canvas (live)" : "snapshot");
|
|
286
|
+
|
|
287
|
+
let sourceEl = el; // element uploaded as the texture in live mode
|
|
288
|
+
if (live) {
|
|
289
|
+
// HTML-in-Canvas: the subtree becomes canvas children (wrapped in
|
|
290
|
+
// a single div we can pass to texElementImage2D) so it keeps
|
|
291
|
+
// layout, interactivity and its accessibility-tree presence while
|
|
292
|
+
// the canvas paints it (WICG proposal; Chrome origin trial behind
|
|
293
|
+
// chrome://flags/#canvas-draw-element). layoutsubtree was already
|
|
294
|
+
// set before context creation above.
|
|
295
|
+
const wrap = document.createElement("div");
|
|
296
|
+
while (el.firstChild) wrap.appendChild(el.firstChild);
|
|
297
|
+
canvas.appendChild(wrap);
|
|
298
|
+
sourceEl = wrap;
|
|
299
|
+
// The host collapses once its children move into the canvas, so
|
|
300
|
+
// the canvas itself carries the box in normal flow (not overlay).
|
|
301
|
+
canvas.style.cssText =
|
|
302
|
+
`display:block;width:${rect.width}px;height:${rect.height}px;`;
|
|
303
|
+
} else {
|
|
304
|
+
// Snapshot mode: purely visual overlay; the real DOM stays put
|
|
305
|
+
// underneath for accessibility, selection and events.
|
|
306
|
+
canvas.style.cssText =
|
|
307
|
+
"position:absolute;top:0;left:0;" +
|
|
308
|
+
`width:${rect.width}px;height:${rect.height}px;` +
|
|
309
|
+
"pointer-events:none;z-index:2147483000;";
|
|
310
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
311
|
+
}
|
|
312
|
+
el.appendChild(canvas);
|
|
313
|
+
|
|
314
|
+
// Compile the pipeline.
|
|
315
|
+
const compile = (type, src) => {
|
|
316
|
+
const s = gl.createShader(type);
|
|
317
|
+
gl.shaderSource(s, src);
|
|
318
|
+
gl.compileShader(s);
|
|
319
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
|
320
|
+
throw new Error("[nodality] raster shader: " + gl.getShaderInfoLog(s));
|
|
321
|
+
}
|
|
322
|
+
return s;
|
|
323
|
+
};
|
|
324
|
+
const prog = gl.createProgram();
|
|
325
|
+
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
|
|
326
|
+
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, buildFragmentShader(rasterNodes)));
|
|
327
|
+
gl.linkProgram(prog);
|
|
328
|
+
gl.useProgram(prog);
|
|
329
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
330
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
331
|
+
const aLoc = gl.getAttribLocation(prog, "a");
|
|
332
|
+
gl.enableVertexAttribArray(aLoc);
|
|
333
|
+
gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
|
|
334
|
+
const U = (n) => gl.getUniformLocation(prog, n);
|
|
335
|
+
|
|
336
|
+
// Static uniforms from the node data.
|
|
337
|
+
rasterNodes.forEach((node, i) => {
|
|
338
|
+
const def = REGISTRY[node.op];
|
|
339
|
+
if (!def || !def.uniforms) return;
|
|
340
|
+
const us = def.uniforms(node, dpr);
|
|
341
|
+
for (const key in us) {
|
|
342
|
+
const [kind, value] = us[key];
|
|
343
|
+
const loc = U(`u${i}_${key}`);
|
|
344
|
+
if (kind === "1f") gl.uniform1f(loc, value);
|
|
345
|
+
else if (kind === "2fv") gl.uniform2fv(loc, value);
|
|
346
|
+
else if (kind === "3fv") gl.uniform3fv(loc, value);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const tex = gl.createTexture();
|
|
351
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
352
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
353
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
354
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
355
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
356
|
+
|
|
357
|
+
let textureReady = false;
|
|
358
|
+
let destroyed = false;
|
|
359
|
+
let mode = live ? "live" : "snapshot";
|
|
360
|
+
|
|
361
|
+
// Snapshot the host's children (excluding our own canvas). Once the
|
|
362
|
+
// texture is live, the host's own painting is hidden via the
|
|
363
|
+
// visibility trick (the canvas child re-shows itself) so transparent
|
|
364
|
+
// hosts — e.g. a bare headline — don't ghost-double under the effect.
|
|
365
|
+
const snapshotCapture = () => {
|
|
366
|
+
canvas.style.display = "none";
|
|
367
|
+
const prevVisibility = el.style.visibility;
|
|
368
|
+
el.style.visibility = "";
|
|
369
|
+
const p = snapshotToImage(el, rect.width, rect.height, dpr)
|
|
370
|
+
.then((img) => {
|
|
371
|
+
if (destroyed) return;
|
|
372
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
373
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
|
|
374
|
+
textureReady = true;
|
|
375
|
+
el.style.visibility = "hidden";
|
|
376
|
+
canvas.style.visibility = "visible";
|
|
377
|
+
})
|
|
378
|
+
.catch((e) => {
|
|
379
|
+
el.style.visibility = prevVisibility;
|
|
380
|
+
console.warn("[nodality] raster snapshot failed:", e);
|
|
381
|
+
});
|
|
382
|
+
canvas.style.display = "";
|
|
383
|
+
return p;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Live uploads are driven by the canvas `paint` event, per the
|
|
387
|
+
// HTML-in-Canvas origin trial: it fires whenever the nested subtree
|
|
388
|
+
// renders or redraws. Uploading outside of it yields an empty
|
|
389
|
+
// (fully transparent) texture — which looks like a blank box.
|
|
390
|
+
//
|
|
391
|
+
// The trial's texElementImage2D signature has drifted across Chrome
|
|
392
|
+
// builds (the element parameter has moved as the arg list shrank),
|
|
393
|
+
// so the working call form is discovered at runtime — guided by the
|
|
394
|
+
// function's declared arity — and cached after the first success.
|
|
395
|
+
let liveForm = -1;
|
|
396
|
+
const onPaint = () => {
|
|
397
|
+
if (destroyed || mode !== "live") return;
|
|
398
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
399
|
+
// Confirmed signature in Chrome 148+ (origin trial):
|
|
400
|
+
// texElementImage2D(target, internalformat, element)
|
|
401
|
+
// where internalformat must be a *sized* WebGL2 format
|
|
402
|
+
// (RGBA8 / SRGB8_ALPHA8 / RGBA16F / RGBA32F) — not gl.RGBA.
|
|
403
|
+
const RGBA8 = gl.RGBA8 || 0x8058;
|
|
404
|
+
const forms = [
|
|
405
|
+
[gl.TEXTURE_2D, RGBA8, sourceEl], // 0 confirmed Chrome 148+
|
|
406
|
+
[gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceEl], // 1 explainer/blog form
|
|
407
|
+
[gl.TEXTURE_2D, 0, RGBA8, sourceEl], // 2 (target, level, internalformat, element)
|
|
408
|
+
[gl.TEXTURE_2D, 0, sourceEl], // 3 (target, level, element)
|
|
409
|
+
];
|
|
410
|
+
let order;
|
|
411
|
+
if (liveForm >= 0) {
|
|
412
|
+
order = [liveForm];
|
|
413
|
+
} else {
|
|
414
|
+
order = [0, 1, 2, 3];
|
|
415
|
+
const byArity = { 3: 0, 6: 1, 4: 2 }[gl.texElementImage2D.length];
|
|
416
|
+
if (byArity != null) {
|
|
417
|
+
order.splice(order.indexOf(byArity), 1);
|
|
418
|
+
order.unshift(byArity);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const errs = [];
|
|
422
|
+
for (const i of order) {
|
|
423
|
+
try {
|
|
424
|
+
gl.texElementImage2D.apply(gl, forms[i]);
|
|
425
|
+
if (liveForm < 0) {
|
|
426
|
+
liveForm = i;
|
|
427
|
+
console.info("[nodality] texElementImage2D signature discovered: form",
|
|
428
|
+
i, "(declared arity " + gl.texElementImage2D.length + ")");
|
|
429
|
+
}
|
|
430
|
+
textureReady = true;
|
|
431
|
+
return;
|
|
432
|
+
} catch (e) {
|
|
433
|
+
errs.push("form" + i + " -> " + (e && e.name ? e.name : "?") + ": " +
|
|
434
|
+
(e && e.message ? e.message : e));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
fallbackToSnapshot("texElementImage2D rejected all known signatures (arity " +
|
|
438
|
+
gl.texElementImage2D.length + "): " + errs.join(" | "));
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// If the live path misbehaves (API surface drift, paint event never
|
|
442
|
+
// firing), never leave a blank box behind: undo the restructuring
|
|
443
|
+
// and continue with the portable snapshot backend.
|
|
444
|
+
const fallbackToSnapshot = (reason) => {
|
|
445
|
+
if (destroyed || mode !== "live") return;
|
|
446
|
+
mode = "snapshot";
|
|
447
|
+
console.warn("[nodality] live raster backend failed (" + reason + ") - falling back to snapshot.");
|
|
448
|
+
canvas.removeEventListener("paint", onPaint);
|
|
449
|
+
while (sourceEl.firstChild) el.appendChild(sourceEl.firstChild);
|
|
450
|
+
if (sourceEl.parentNode === canvas) canvas.removeChild(sourceEl);
|
|
451
|
+
canvas.removeAttribute("layoutsubtree");
|
|
452
|
+
canvas.style.cssText =
|
|
453
|
+
"position:absolute;top:0;left:0;" +
|
|
454
|
+
`width:${rect.width}px;height:${rect.height}px;` +
|
|
455
|
+
"pointer-events:none;z-index:2147483000;";
|
|
456
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
457
|
+
canvas.setAttribute("data-nodality-raster", "snapshot-fallback");
|
|
458
|
+
snapshotCapture();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
if (live) {
|
|
462
|
+
canvas.addEventListener("paint", onPaint);
|
|
463
|
+
setTimeout(() => {
|
|
464
|
+
if (!destroyed && mode === "live" && !textureReady) {
|
|
465
|
+
fallbackToSnapshot("no paint event within 1500ms");
|
|
466
|
+
}
|
|
467
|
+
}, 1500);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Pointer -> u_mouse (listen on the host so the overlay stays
|
|
471
|
+
// pointer-events: none).
|
|
472
|
+
let mouse = [canvas.width * 0.5, canvas.height * 0.5];
|
|
473
|
+
const onMove = (e) => {
|
|
474
|
+
const r = canvas.getBoundingClientRect();
|
|
475
|
+
const pt = e.touches ? e.touches[0] : e;
|
|
476
|
+
mouse = [(pt.clientX - r.left) * dpr, (pt.clientY - r.top) * dpr];
|
|
477
|
+
};
|
|
478
|
+
el.addEventListener("mousemove", onMove, { passive: true });
|
|
479
|
+
el.addEventListener("touchmove", onMove, { passive: true });
|
|
480
|
+
|
|
481
|
+
// Render loop — paused while off-screen.
|
|
482
|
+
let raf = 0;
|
|
483
|
+
let visible = true;
|
|
484
|
+
const draw = () => {
|
|
485
|
+
if (destroyed) return;
|
|
486
|
+
if (visible && textureReady) {
|
|
487
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
488
|
+
gl.clearColor(0, 0, 0, 0);
|
|
489
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
490
|
+
gl.uniform2f(U("u_res"), canvas.width, canvas.height);
|
|
491
|
+
gl.uniform2f(U("u_mouse"), mouse[0], mouse[1]);
|
|
492
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
493
|
+
}
|
|
494
|
+
raf = requestAnimationFrame(draw);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
let io = null;
|
|
498
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
499
|
+
io = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; });
|
|
500
|
+
io.observe(el);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let ro = null;
|
|
504
|
+
let resizeTimer = 0;
|
|
505
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
506
|
+
ro = new ResizeObserver(() => {
|
|
507
|
+
clearTimeout(resizeTimer);
|
|
508
|
+
resizeTimer = setTimeout(() => {
|
|
509
|
+
if (destroyed || mode !== "snapshot") return;
|
|
510
|
+
const m = measure();
|
|
511
|
+
rect.width = m.width;
|
|
512
|
+
rect.height = m.height;
|
|
513
|
+
canvas.width = Math.round(m.width * dpr);
|
|
514
|
+
canvas.height = Math.round(m.height * dpr);
|
|
515
|
+
canvas.style.width = m.width + "px";
|
|
516
|
+
canvas.style.height = m.height + "px";
|
|
517
|
+
snapshotCapture();
|
|
518
|
+
}, 150);
|
|
519
|
+
});
|
|
520
|
+
ro.observe(el);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Window-resize handling for both modes. This backs up the
|
|
524
|
+
// ResizeObserver above: RO callbacks ride the rendering frame loop
|
|
525
|
+
// and stall in backgrounded tabs, while the resize event always
|
|
526
|
+
// fires. Snapshot mode re-measures the host and recaptures. Live
|
|
527
|
+
// mode is RO-blind anyway (the host is collapsed; its children live
|
|
528
|
+
// inside the canvas): widen the canvas to the host's flow width,
|
|
529
|
+
// let the subtree reflow, then fit the height and re-upload.
|
|
530
|
+
let winResizeTimer = 0;
|
|
531
|
+
const onWinResize = () => {
|
|
532
|
+
clearTimeout(winResizeTimer);
|
|
533
|
+
winResizeTimer = setTimeout(() => {
|
|
534
|
+
if (destroyed) return;
|
|
535
|
+
if (mode === "snapshot") {
|
|
536
|
+
const m = measure();
|
|
537
|
+
rect.width = m.width;
|
|
538
|
+
rect.height = m.height;
|
|
539
|
+
canvas.width = Math.round(m.width * dpr);
|
|
540
|
+
canvas.height = Math.round(m.height * dpr);
|
|
541
|
+
canvas.style.width = m.width + "px";
|
|
542
|
+
canvas.style.height = m.height + "px";
|
|
543
|
+
snapshotCapture();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const w = el.clientWidth ||
|
|
547
|
+
(el.parentElement && el.parentElement.clientWidth) || rect.width;
|
|
548
|
+
canvas.style.width = w + "px";
|
|
549
|
+
setTimeout(() => {
|
|
550
|
+
if (destroyed || mode !== "live") return;
|
|
551
|
+
const h = Math.max(2, Math.round(sourceEl.getBoundingClientRect().height)) || rect.height;
|
|
552
|
+
rect.width = w;
|
|
553
|
+
rect.height = h;
|
|
554
|
+
canvas.style.height = h + "px";
|
|
555
|
+
canvas.width = Math.round(w * dpr);
|
|
556
|
+
canvas.height = Math.round(h * dpr);
|
|
557
|
+
onPaint(); // fresh upload at the new size
|
|
558
|
+
}, 50);
|
|
559
|
+
}, 150);
|
|
560
|
+
};
|
|
561
|
+
if (typeof window !== "undefined") {
|
|
562
|
+
window.addEventListener("resize", onWinResize);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (mode === "live") {
|
|
566
|
+
// Texture arrives via the paint event; start drawing right away.
|
|
567
|
+
raf = requestAnimationFrame(draw);
|
|
568
|
+
} else {
|
|
569
|
+
snapshotCapture().then(() => { raf = requestAnimationFrame(draw); });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
canvas,
|
|
574
|
+
refresh: () => (mode === "live" ? onPaint() : snapshotCapture()),
|
|
575
|
+
destroy() {
|
|
576
|
+
destroyed = true;
|
|
577
|
+
cancelAnimationFrame(raf);
|
|
578
|
+
clearTimeout(resizeTimer);
|
|
579
|
+
clearTimeout(winResizeTimer);
|
|
580
|
+
if (io) io.disconnect();
|
|
581
|
+
if (ro) ro.disconnect();
|
|
582
|
+
if (typeof window !== "undefined") window.removeEventListener("resize", onWinResize);
|
|
583
|
+
el.removeEventListener("mousemove", onMove);
|
|
584
|
+
el.removeEventListener("touchmove", onMove);
|
|
585
|
+
canvas.removeEventListener("paint", onPaint);
|
|
586
|
+
while (sourceEl !== el && sourceEl.firstChild) el.appendChild(sourceEl.firstChild);
|
|
587
|
+
el.style.visibility = "";
|
|
588
|
+
canvas.remove();
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export { applyRasterPipeline, registerRasterOp, RASTER_OP_NAMES, isHTMLInCanvasAvailable };
|
package/lib/scroll-video.js
CHANGED
package/lib/seo.js
CHANGED
package/lib/stacker.js
CHANGED
package/lib/theme.js
CHANGED
package/lib/transform-anim.js
CHANGED