distortion-wave 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +204 -2
- package/dist/distortion-wave.iife.min.js +159 -0
- package/dist/distortion-wave.js +1409 -0
- package/dist/distortion-wave.min.js +159 -0
- package/package.json +61 -4
- package/src/capture.js +295 -0
- package/src/core.js +169 -0
- package/src/focus.js +34 -0
- package/src/index.js +281 -0
- package/src/renderer.js +295 -0
- package/types/index.d.ts +81 -0
package/src/renderer.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// renderer.js — WebGL2 renderer: one full-screen composite pass + points, blur pyramid, context pool
|
|
2
|
+
// Part of distortion-wave · MIT © Lucas Bonomi
|
|
3
|
+
|
|
4
|
+
import { PALETTES, hexToOklab } from './core.js';
|
|
5
|
+
|
|
6
|
+
// ═══ Shaders (GLSL ES 3.00) ══════════════════════════════════════════════════
|
|
7
|
+
const VS_FULL = `#version 300 es
|
|
8
|
+
out vec2 vUv;
|
|
9
|
+
void main() { vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); vUv = p; gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); }`;
|
|
10
|
+
const FS_COPY = `#version 300 es
|
|
11
|
+
precision highp float; in vec2 vUv; uniform sampler2D uTex; uniform float uLod; out vec4 o;
|
|
12
|
+
void main() { o = textureLod(uTex, vUv, uLod); }`;
|
|
13
|
+
const FS_BLUR = `#version 300 es
|
|
14
|
+
precision highp float; in vec2 vUv; uniform sampler2D uTex; uniform vec2 uStep; uniform float uSigma; out vec4 o;
|
|
15
|
+
void main() {
|
|
16
|
+
float R = min(ceil(uSigma * 3.0), 32.0); vec4 acc = vec4(0.0); float ws = 0.0;
|
|
17
|
+
for (int i = -32; i <= 32; i++) { float fi = float(i); if (abs(fi) > R) continue;
|
|
18
|
+
float w = exp(-0.5 * fi * fi / (uSigma * uSigma)); acc += textureLod(uTex, vUv + uStep * fi, 0.0) * w; ws += w; }
|
|
19
|
+
o = acc / ws;
|
|
20
|
+
}`;
|
|
21
|
+
// All colours are PREMULTIPLIED (the canvas is transparent over the page).
|
|
22
|
+
const FS_MAIN = `#version 300 es
|
|
23
|
+
precision highp float; precision highp int;
|
|
24
|
+
uniform vec2 uSize, uRes;
|
|
25
|
+
uniform sampler2D uOld0, uOld1, uOld2, uNew0, uNew1, uNew2;
|
|
26
|
+
uniform vec4 uOldRect, uNewRect, uShape, uRadii; // rects in canvas CSS px; radii (br, tr, bl, tl)
|
|
27
|
+
uniform int uMode; // 0 before · 1 live · 2 after · 3 crossfade · 4 effects only (live DOM below)
|
|
28
|
+
uniform float uFade;
|
|
29
|
+
uniform bool uLaunched, uRing;
|
|
30
|
+
uniform vec2 uOrigin;
|
|
31
|
+
uniform float uTilt, uSpine, uRadius, uSquash, uTime, uSwell, uEnergy, uPull, uGlow, uRim, uClear, uHairClear, uHold, uK, uSeam;
|
|
32
|
+
uniform float uAmp, uLensW, uAberr, uBlurMax, uBlurNear, uBlurAhead, uBlurBehind;
|
|
33
|
+
uniform float uLead, uTail, uBand, uRimW, uRimSoft, uUneven, uHairW;
|
|
34
|
+
uniform vec3 uPal[8]; uniform int uPalN;
|
|
35
|
+
out vec4 outColor;
|
|
36
|
+
const float TAU = 6.28318530718;
|
|
37
|
+
|
|
38
|
+
mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, s, -s, c); }
|
|
39
|
+
float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
|
|
40
|
+
float h11(float x) { return fract(sin(x * 127.1) * 43758.5453); }
|
|
41
|
+
float vnoise(float x) { float i = floor(x), f = fract(x); return mix(h11(i), h11(i + 1.0), f * f * (3.0 - 2.0 * f)) * 2.0 - 1.0; }
|
|
42
|
+
float sdRoundBox(vec2 p, vec2 b, vec4 r) { // y-down space: r = (bottom-right, top-right, bottom-left, top-left)
|
|
43
|
+
r.xy = (p.x > 0.0) ? r.xy : r.zw; r.x = (p.y > 0.0) ? r.x : r.y;
|
|
44
|
+
vec2 q = abs(p) - b + r.x; return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
|
|
45
|
+
}
|
|
46
|
+
struct Front { float d; vec2 n; vec2 q; float g; };
|
|
47
|
+
Front front(vec2 p) {
|
|
48
|
+
vec2 q = rot(-uTilt) * (p - uOrigin); float mx = q.x * uSquash;
|
|
49
|
+
vec2 v = vec2(max(abs(mx) - uSpine, 0.0), q.y); float len = max(length(v), 1e-3);
|
|
50
|
+
vec2 grad = vec2(v.x * sign(q.x) * uSquash, v.y) / len; float g = max(length(grad), 1e-3);
|
|
51
|
+
float wob;
|
|
52
|
+
if (uRing) { float a = atan(q.y, q.x); wob = 0.5 * sin(3.0 * a + uTime * 1.3) + 0.3 * sin(5.0 * a - uTime * 0.9 + 2.1) + 0.2 * sin(2.0 * a + uTime * 2.0); }
|
|
53
|
+
else { float s = mx * 0.005 / uK; wob = 0.7 * vnoise(s + uTime * 0.8) + 0.3 * vnoise(s * 2.3 - uTime * 1.1 + 5.0); }
|
|
54
|
+
Front f; f.d = (len - uRadius) / g + uSwell * wob; f.n = rot(uTilt) * (grad / g); f.q = q; f.g = g; return f;
|
|
55
|
+
}
|
|
56
|
+
// The silhouette is decided by p (undisplaced); the sample by sp (displaced, clamped to the rect).
|
|
57
|
+
// → content bends INSIDE the element, never revealing transparency or bleeding outside.
|
|
58
|
+
vec4 tap(sampler2D s, vec4 rect, vec2 p, vec2 sp) {
|
|
59
|
+
vec2 q = (p - rect.xy) / rect.zw;
|
|
60
|
+
if (q.x < 0.0 || q.y < 0.0 || q.x > 1.0 || q.y > 1.0) return vec4(0.0);
|
|
61
|
+
return textureLod(s, clamp((sp - rect.xy) / rect.zw, 0.0, 1.0), 0.0);
|
|
62
|
+
}
|
|
63
|
+
vec4 fetch3(sampler2D s0, sampler2D s1, sampler2D s2, vec4 rect, vec2 p, vec2 sp, float tn, float tf) {
|
|
64
|
+
vec4 c = tap(s0, rect, p, sp);
|
|
65
|
+
if (tn > 0.0) c = mix(c, tap(s1, rect, p, sp), tn);
|
|
66
|
+
if (tf > 0.0) c = mix(c, tap(s2, rect, p, sp), tf);
|
|
67
|
+
return c;
|
|
68
|
+
}
|
|
69
|
+
vec4 layerOld(vec2 p, vec2 sp, vec2 sl, float tn, float tf) {
|
|
70
|
+
if (dot(sl, sl) < 0.01) return fetch3(uOld0, uOld1, uOld2, uOldRect, p, sp, tn, tf);
|
|
71
|
+
vec4 r = fetch3(uOld0, uOld1, uOld2, uOldRect, p, sp + sl, tn, tf), g = fetch3(uOld0, uOld1, uOld2, uOldRect, p, sp, tn, tf), b = fetch3(uOld0, uOld1, uOld2, uOldRect, p, sp - sl, tn, tf);
|
|
72
|
+
return vec4(r.r, g.g, b.b, max(max(r.a, g.a), b.a));
|
|
73
|
+
}
|
|
74
|
+
vec4 layerNew(vec2 p, vec2 sp, vec2 sl, float tn, float tf) {
|
|
75
|
+
if (dot(sl, sl) < 0.01) return fetch3(uNew0, uNew1, uNew2, uNewRect, p, sp, tn, tf);
|
|
76
|
+
vec4 r = fetch3(uNew0, uNew1, uNew2, uNewRect, p, sp + sl, tn, tf), g = fetch3(uNew0, uNew1, uNew2, uNewRect, p, sp, tn, tf), b = fetch3(uNew0, uNew1, uNew2, uNewRect, p, sp - sl, tn, tf);
|
|
77
|
+
return vec4(r.r, g.g, b.b, max(max(r.a, g.a), b.a));
|
|
78
|
+
}
|
|
79
|
+
vec3 paletteAt(float u) {
|
|
80
|
+
float x = fract(u) * float(uPalN); int i = min(int(floor(x)), uPalN - 1); int j = (i + 1) % uPalN;
|
|
81
|
+
return mix(uPal[i], uPal[j], smoothstep(0.0, 1.0, fract(x)));
|
|
82
|
+
}
|
|
83
|
+
vec3 oklabToSrgb(vec3 c) {
|
|
84
|
+
float l = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z, m = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z, s = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;
|
|
85
|
+
l = l * l * l; m = m * m * m; s = s * s * s;
|
|
86
|
+
vec3 lin = clamp(vec3(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s), 0.0, 1.0);
|
|
87
|
+
return mix(12.92 * lin, 1.055 * pow(lin, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), lin));
|
|
88
|
+
}
|
|
89
|
+
void main() {
|
|
90
|
+
vec2 p = vec2(gl_FragCoord.x, uRes.y - gl_FragCoord.y) * (uSize.x / uRes.x);
|
|
91
|
+
float dn = (ign(gl_FragCoord.xy) - 0.5) / 255.0;
|
|
92
|
+
if (uMode == 0 || uMode == 2 || uMode == 3) {
|
|
93
|
+
vec4 a = tap(uOld0, uOldRect, p, p), b = tap(uNew0, uNewRect, p, p);
|
|
94
|
+
vec4 c = uMode == 0 ? a : uMode == 2 ? b : mix(a, b, uFade);
|
|
95
|
+
outColor = vec4(c.rgb + dn * c.a, c.a); return;
|
|
96
|
+
}
|
|
97
|
+
Front f = front(p);
|
|
98
|
+
vec2 r = p - uOrigin; float rl = max(length(r), 1.0);
|
|
99
|
+
float held = uHold > 0.0 ? smoothstep(uHold * 0.9, uHold * 1.8, rl) : 1.0;
|
|
100
|
+
vec4 col = vec4(0.0);
|
|
101
|
+
if (uMode == 1) {
|
|
102
|
+
float k = f.d / (uLensW * (f.d > 0.0 ? 0.8 : 1.6));
|
|
103
|
+
vec2 bulge = f.n * (uEnergy * uAmp * exp(-k * k) * held);
|
|
104
|
+
vec2 suck = (r / rl) * (uPull * (rl / (rl + 60.0 * uK)) * exp(-rl / (700.0 * uK)) * held);
|
|
105
|
+
vec2 sp = p - bulge + suck;
|
|
106
|
+
vec2 sl = (bulge * 0.5 - suck * exp(-rl / (240.0 * uK))) * uAberr;
|
|
107
|
+
float kb = f.d / (f.d > 0.0 ? uBlurAhead : uBlurBehind);
|
|
108
|
+
float bpx = uEnergy * uBlurMax * exp(-kb * kb) * held;
|
|
109
|
+
float tn = smoothstep(0.0, uBlurNear, bpx);
|
|
110
|
+
float tf = uBlurMax > uBlurNear ? smoothstep(uBlurNear, uBlurMax, bpx) : 0.0;
|
|
111
|
+
float cover = uLaunched ? smoothstep(-uSeam, uSeam, f.d) : 1.0;
|
|
112
|
+
if (cover >= 0.999) col = layerOld(p, sp, sl, tn, tf);
|
|
113
|
+
else if (cover <= 0.001) col = layerNew(p, sp, sl, tn, tf);
|
|
114
|
+
else col = mix(layerNew(p, sp, sl, tn, tf), layerOld(p, sp, sl, tn, tf), cover);
|
|
115
|
+
// the UNdisplaced alpha fixes the silhouette (rounded corners, transparent areas): no fill-in, no fringe outside
|
|
116
|
+
float aU = mix(tap(uNew0, uNewRect, p, p).a, tap(uOld0, uOldRect, p, p).a, cover);
|
|
117
|
+
col *= min(1.0, aU / max(col.a, 1e-4));
|
|
118
|
+
}
|
|
119
|
+
// element shape (per-corner radii): mask + rim
|
|
120
|
+
vec2 c = p - (uShape.xy + uShape.zw * 0.5);
|
|
121
|
+
float sd = sdRoundBox(c, uShape.zw * 0.5, uRadii);
|
|
122
|
+
float mask = clamp(0.5 - sd * (uRes.x / uSize.x), 0.0, 1.0);
|
|
123
|
+
float inset = max(-sd, 0.0);
|
|
124
|
+
float along = f.q.x * 0.0065 / uK, ang = atan(f.q.y, f.q.x);
|
|
125
|
+
float lobes = uRing ? 0.5 + 0.5 * sin(2.0 * ang + uTime * 1.4) * cos(ang - uTime * 0.6 + 0.8)
|
|
126
|
+
: 0.5 + 0.5 * sin(along * 1.4 + uTime * 1.3) * cos(along * 0.7 - uTime * 0.6 + 0.8);
|
|
127
|
+
float bandA = 0.0;
|
|
128
|
+
if (uGlow > 0.0 && f.d < uLead) {
|
|
129
|
+
if (f.d >= 0.0) { float x = f.d / uLead; bandA = exp(-4.5 * x * x); }
|
|
130
|
+
else { float x = clamp(-f.d / (uTail * mix(0.55, 1.35, lobes)), 0.0, 1.0); bandA = pow(1.0 - x, 2.4); }
|
|
131
|
+
bandA *= uGlow * uBand * held;
|
|
132
|
+
}
|
|
133
|
+
float around = atan(c.y / uShape.w, c.x / uShape.z);
|
|
134
|
+
float rimA = 0.0, hairA = 0.0, shimmer = 0.5;
|
|
135
|
+
if (uRim > 0.0 && inset < uRimW * 4.5) {
|
|
136
|
+
float lumps = 0.5 * sin(around * 3.0 + uTime * 0.7) + 0.3 * sin(around * 5.0 - uTime * 1.2 + 2.0) + 0.2 * sin(around * 9.0 + uTime * 2.3 + 4.0);
|
|
137
|
+
float thick = max(1.0 + uUneven * 0.55 * lumps, 0.15);
|
|
138
|
+
float hk = inset / (uRimW * thick);
|
|
139
|
+
float reached = 1.0 - smoothstep(-uLead * 0.6, uLead * 0.4, f.d);
|
|
140
|
+
float kept = smoothstep(-uRimSoft, uRimSoft, f.d + (uRadius - uClear) / f.g);
|
|
141
|
+
shimmer = 0.5 + 0.5 * sin(uTime * 3.2 - around * 4.0);
|
|
142
|
+
rimA = exp(-hk * hk) * reached * kept * uRim * (0.72 + 0.28 * shimmer);
|
|
143
|
+
float line = 1.0 - smoothstep(uHairW * 0.5, uHairW * 0.5 + 0.8, inset);
|
|
144
|
+
float hairKept = smoothstep(-uRimSoft, uRimSoft, f.d + (uRadius - uHairClear) / f.g);
|
|
145
|
+
hairA = line * reached * hairKept * uRim * 0.85 * mix(0.55, 1.0, smoothstep(0.2, 1.4, thick));
|
|
146
|
+
}
|
|
147
|
+
float a = (1.0 - (1.0 - bandA) * (1.0 - rimA)) * mask;
|
|
148
|
+
hairA *= mask;
|
|
149
|
+
if (a + hairA < 0.0005) { outColor = vec4(col.rgb + dn * col.a, col.a); return; }
|
|
150
|
+
float flow = 0.6 + (uRing ? ang / TAU : along * 0.22) + uTime * 0.12 + f.d * 0.0016 / uK;
|
|
151
|
+
vec3 lab = paletteAt(flow);
|
|
152
|
+
lab.x += 0.10 * exp(-pow((f.d - 28.0 * uK) / (48.0 * uK), 2.0)) - 0.12 * exp(-pow((f.d + 90.0 * uK) / (70.0 * uK), 2.0));
|
|
153
|
+
lab.yz *= 1.2;
|
|
154
|
+
lab = mix(lab, vec3(0.985, 0.0, 0.0), 0.65 * exp(-pow((f.d + 6.0 * uK) / max(24.0 * uK, 3.0), 2.0)));
|
|
155
|
+
vec3 rimLab = paletteAt(around / TAU + uTime * 0.25 + inset * 0.0012 / uK); rimLab.yz *= 1.1;
|
|
156
|
+
lab = mix(lab, rimLab, rimA / (rimA + bandA + 1e-4));
|
|
157
|
+
float ea = clamp(a, 0.0, 1.0);
|
|
158
|
+
vec4 o = vec4(oklabToSrgb(lab) * ea, ea) + col * (1.0 - ea);
|
|
159
|
+
float ha = clamp(hairA, 0.0, 1.0);
|
|
160
|
+
o = vec4(mix(oklabToSrgb(rimLab), vec3(1.0), 0.6 + 0.35 * shimmer) * ha, ha) + o * (1.0 - ha);
|
|
161
|
+
outColor = vec4(o.rgb + dn * 2.0 * o.a, o.a);
|
|
162
|
+
}`;
|
|
163
|
+
const VS_DOTS = `#version 300 es
|
|
164
|
+
layout(location = 0) in vec4 aDot; uniform vec2 uSize; uniform float uDpr; out float vA; out float vR; out float vPt;
|
|
165
|
+
void main() { gl_Position = vec4(aDot.x / uSize.x * 2.0 - 1.0, 1.0 - aDot.y / uSize.y * 2.0, 0.0, 1.0);
|
|
166
|
+
vPt = ceil((aDot.z + 1.0) * uDpr) + 2.0; gl_PointSize = vPt; vR = aDot.z * 0.5 * uDpr; vA = aDot.w; }`;
|
|
167
|
+
const FS_DOTS = `#version 300 es
|
|
168
|
+
precision highp float; in float vA; in float vR; in float vPt; out vec4 o;
|
|
169
|
+
void main() { float r = length(gl_PointCoord - 0.5) * vPt; float cov = clamp(vR - r + 0.5, 0.0, 1.0); o = vec4(vec3(cov * vA), cov * vA); }`;
|
|
170
|
+
|
|
171
|
+
// ═══ WebGL2 renderer (one canvas per active transition, pooled) ══════════════
|
|
172
|
+
function program(gl, vs, fs) {
|
|
173
|
+
const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
|
|
174
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s) || 'compile'); return s; };
|
|
175
|
+
const p = gl.createProgram();
|
|
176
|
+
gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs)); gl.linkProgram(p);
|
|
177
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p) || 'link');
|
|
178
|
+
const locs = new Map();
|
|
179
|
+
return { p, u: (n) => { if (!locs.has(n)) locs.set(n, gl.getUniformLocation(p, n)); return locs.get(n); } };
|
|
180
|
+
}
|
|
181
|
+
class Renderer {
|
|
182
|
+
constructor() {
|
|
183
|
+
this.canvas = document.createElement('canvas');
|
|
184
|
+
this.canvas.setAttribute('data-dw-ignore', '');
|
|
185
|
+
this.canvas.setAttribute('aria-hidden', 'true');
|
|
186
|
+
this.canvas.style.cssText = 'position:fixed;left:0;top:0;pointer-events:none;z-index:2147483000;contain:strict;';
|
|
187
|
+
this.layers = {}; this.pal = new Float32Array(24); this.palKey = ''; this.ok = false;
|
|
188
|
+
this.canvas.addEventListener('webglcontextlost', (e) => { e.preventDefault(); this.ok = false; });
|
|
189
|
+
const gl = this.canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: false });
|
|
190
|
+
if (!gl) return;
|
|
191
|
+
this.gl = gl;
|
|
192
|
+
this.main = program(gl, VS_FULL, FS_MAIN); this.blur = program(gl, VS_FULL, FS_BLUR);
|
|
193
|
+
this.copy = program(gl, VS_FULL, FS_COPY); this.dots = program(gl, VS_DOTS, FS_DOTS);
|
|
194
|
+
this.vao = gl.createVertexArray();
|
|
195
|
+
this.dvao = gl.createVertexArray(); this.dbuf = gl.createBuffer(); this.dcap = 0;
|
|
196
|
+
gl.bindVertexArray(this.dvao); gl.bindBuffer(gl.ARRAY_BUFFER, this.dbuf);
|
|
197
|
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 16, 0); gl.bindVertexArray(null);
|
|
198
|
+
this.maxTex = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
199
|
+
this.ok = true;
|
|
200
|
+
}
|
|
201
|
+
tex(w, h, src, mips = false) {
|
|
202
|
+
const gl = this.gl, t = gl.createTexture();
|
|
203
|
+
gl.bindTexture(gl.TEXTURE_2D, t);
|
|
204
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
205
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); // correct blur on transparent edges
|
|
206
|
+
if (src) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, src);
|
|
207
|
+
else gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
208
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mips ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
|
|
209
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
210
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
211
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
212
|
+
if (mips) gl.generateMipmap(gl.TEXTURE_2D);
|
|
213
|
+
return t;
|
|
214
|
+
}
|
|
215
|
+
target(w, h) {
|
|
216
|
+
const gl = this.gl, tex = this.tex(w, h, null), fb = gl.createFramebuffer();
|
|
217
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
218
|
+
return { tex, fb };
|
|
219
|
+
}
|
|
220
|
+
pass(prog, fb, w, h) { const gl = this.gl; gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.viewport(0, 0, w, h); gl.useProgram(prog.p); gl.bindVertexArray(this.vao); gl.drawArrays(gl.TRIANGLES, 0, 3); }
|
|
221
|
+
blurred(sharp, w, h, sigma) {
|
|
222
|
+
const gl = this.gl, scale = Math.max(1, Math.round(sigma / 4));
|
|
223
|
+
const bw = Math.max(1, Math.round(w / scale)), bh = Math.max(1, Math.round(h / scale));
|
|
224
|
+
const A = this.target(bw, bh), B = this.target(bw, bh);
|
|
225
|
+
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, sharp);
|
|
226
|
+
gl.useProgram(this.copy.p); gl.uniform1i(this.copy.u('uTex'), 0); gl.uniform1f(this.copy.u('uLod'), Math.log2(scale));
|
|
227
|
+
this.pass(this.copy, A.fb, bw, bh);
|
|
228
|
+
gl.useProgram(this.blur.p); gl.uniform1i(this.blur.u('uTex'), 0); gl.uniform1f(this.blur.u('uSigma'), Math.max(sigma / scale, 0.01));
|
|
229
|
+
gl.bindTexture(gl.TEXTURE_2D, A.tex); gl.uniform2f(this.blur.u('uStep'), 1 / bw, 0); this.pass(this.blur, B.fb, bw, bh);
|
|
230
|
+
gl.bindTexture(gl.TEXTURE_2D, B.tex); gl.uniform2f(this.blur.u('uStep'), 0, 1 / bh); this.pass(this.blur, A.fb, bw, bh);
|
|
231
|
+
gl.deleteFramebuffer(B.fb); gl.deleteTexture(B.tex); gl.deleteFramebuffer(A.fb);
|
|
232
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
233
|
+
return A.tex;
|
|
234
|
+
}
|
|
235
|
+
/** Builds sharp + 2 blur levels (once per capture, never per frame). */
|
|
236
|
+
setLayer(name, src, sigmaNear, sigmaFar) {
|
|
237
|
+
if (!this.ok) return;
|
|
238
|
+
this.dropLayer(name);
|
|
239
|
+
const sharp = this.tex(src.width, src.height, src, true);
|
|
240
|
+
this.layers[name] = { sharp, near: this.blurred(sharp, src.width, src.height, sigmaNear), far: this.blurred(sharp, src.width, src.height, sigmaFar) };
|
|
241
|
+
}
|
|
242
|
+
dropLayer(name) { const L = this.layers[name]; if (!L || !this.gl) return; for (const t of [L.sharp, L.near, L.far]) this.gl.deleteTexture(t); delete this.layers[name]; }
|
|
243
|
+
clear() { for (const n of Object.keys(this.layers)) this.dropLayer(n); }
|
|
244
|
+
render(f) {
|
|
245
|
+
if (!this.ok) return;
|
|
246
|
+
const gl = this.gl, W = this.canvas.width, H = this.canvas.height, m = this.main, u = m.u;
|
|
247
|
+
const old = this.layers.old || this.layers.new, nxt = this.layers.new || this.layers.old;
|
|
248
|
+
if (!old) return;
|
|
249
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, W, H); gl.disable(gl.BLEND); gl.useProgram(m.p);
|
|
250
|
+
[['uOld0', old.sharp], ['uOld1', old.near], ['uOld2', old.far], ['uNew0', nxt.sharp], ['uNew1', nxt.near], ['uNew2', nxt.far]]
|
|
251
|
+
.forEach(([n, t], i) => { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, t); gl.uniform1i(u(n), i); });
|
|
252
|
+
const R = f.rects, v4 = (n, r) => gl.uniform4f(u(n), r.x, r.y, r.w, r.h);
|
|
253
|
+
gl.uniform2f(u('uSize'), f.w, f.h); gl.uniform2f(u('uRes'), W, H);
|
|
254
|
+
v4('uOldRect', R.old); v4('uNewRect', R.new || R.old); v4('uShape', R.shape);
|
|
255
|
+
gl.uniform4f(u('uRadii'), R.radii.br, R.radii.tr, R.radii.bl, R.radii.tl);
|
|
256
|
+
gl.uniform1i(u('uMode'), f.mode); gl.uniform1f(u('uFade'), f.fade || 0);
|
|
257
|
+
const s = f.s, c = f.cfg;
|
|
258
|
+
if (s) {
|
|
259
|
+
gl.uniform1i(u('uLaunched'), s.launched ? 1 : 0); gl.uniform1i(u('uRing'), s.ring ? 1 : 0);
|
|
260
|
+
gl.uniform2f(u('uOrigin'), s.ox, s.oy);
|
|
261
|
+
const fl = { uTilt: s.tilt, uSpine: s.spine, uRadius: s.radius, uSquash: s.squash, uTime: s.time, uSwell: s.swell, uEnergy: s.energy,
|
|
262
|
+
uPull: s.pull, uGlow: s.glow, uRim: s.rim, uClear: s.clear, uHairClear: s.hairClear, uHold: s.hold, uK: c.k, uSeam: c.seam,
|
|
263
|
+
uAmp: c.amplitude, uLensW: c.lensWidth, uAberr: c.aberration, uBlurMax: c.blur, uBlurNear: c.blurNear, uBlurAhead: c.blurAhead,
|
|
264
|
+
uBlurBehind: c.blurBehind, uLead: c.lead, uTail: c.tail, uBand: c.band, uRimW: c.rimWidth, uRimSoft: c.rimSoftness,
|
|
265
|
+
uUneven: c.uneven, uHairW: c.hairWidth };
|
|
266
|
+
for (const k in fl) gl.uniform1f(u(k), fl[k]);
|
|
267
|
+
const cols = Array.isArray(c.palette) ? c.palette : PALETTES[c.palette] || PALETTES.aurora;
|
|
268
|
+
const key = cols.join();
|
|
269
|
+
if (this.palKey !== key) { this.palKey = key; cols.slice(0, 8).forEach((h, i) => this.pal.set(hexToOklab(h), i * 3)); }
|
|
270
|
+
gl.uniform3fv(u('uPal[0]'), this.pal); gl.uniform1i(u('uPalN'), Math.min(cols.length, 8));
|
|
271
|
+
}
|
|
272
|
+
gl.bindVertexArray(this.vao); gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
273
|
+
if (f.dotCount > 0) {
|
|
274
|
+
const d = this.dots;
|
|
275
|
+
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
276
|
+
gl.useProgram(d.p); gl.uniform2f(d.u('uSize'), f.w, f.h); gl.uniform1f(d.u('uDpr'), f.dpr);
|
|
277
|
+
gl.bindVertexArray(this.dvao); gl.bindBuffer(gl.ARRAY_BUFFER, this.dbuf);
|
|
278
|
+
if (this.dcap < f.dots.length) { gl.bufferData(gl.ARRAY_BUFFER, f.dots.byteLength, gl.DYNAMIC_DRAW); this.dcap = f.dots.length; }
|
|
279
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, f.dots, 0, f.dotCount * 4);
|
|
280
|
+
gl.drawArrays(gl.POINTS, 0, f.dotCount); gl.disable(gl.BLEND);
|
|
281
|
+
}
|
|
282
|
+
gl.bindVertexArray(null);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Pool: creating a WebGL context + compiling shaders costs tens of ms → reuse them.
|
|
286
|
+
const pool = []; let alive = 0; const MAX_CONTEXTS = 4;
|
|
287
|
+
export function acquire() {
|
|
288
|
+
while (pool.length) { const r = pool.pop(); if (r.ok) return r; alive--; }
|
|
289
|
+
if (alive >= MAX_CONTEXTS) return null;
|
|
290
|
+
try { const r = new Renderer(); if (!r.ok) return null; alive++; return r; } catch (e) { console.warn('[distortion-wave]', e); return null; }
|
|
291
|
+
}
|
|
292
|
+
export function release(r) { r.clear(); r.canvas.remove(); if (r.ok) pool.push(r); else alive--; }
|
|
293
|
+
/** Call at load/idle time: compiles the shaders before the first interaction. */
|
|
294
|
+
export function warmupWave() { const r = acquire(); if (r) release(r); return !!r; }
|
|
295
|
+
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Type definitions for distortion-wave
|
|
2
|
+
|
|
3
|
+
export type Variant = 'auto' | 'vertical' | 'horizontal' | 'center';
|
|
4
|
+
export type PaletteName = 'aurora' | 'ocean' | 'mint' | 'ember';
|
|
5
|
+
export type Point = { x: number; y: number };
|
|
6
|
+
export type RectLike = { x: number; y: number; width: number; height: number };
|
|
7
|
+
|
|
8
|
+
export interface Metrics {
|
|
9
|
+
/** ms spent capturing the element before the update */
|
|
10
|
+
captureBefore?: number;
|
|
11
|
+
/** ms spent capturing the element after the update */
|
|
12
|
+
captureAfter?: number;
|
|
13
|
+
/** ms spent uploading + blurring the first capture */
|
|
14
|
+
upload?: number;
|
|
15
|
+
/** longest frame seen during the transition, in ms */
|
|
16
|
+
worstFrame?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DistortionWaveOptions {
|
|
20
|
+
/** Where the wave starts. An element (inside or outside the target), a rect, a point, or nothing (touch point, else center). */
|
|
21
|
+
source?: Element | RectLike | Point | null;
|
|
22
|
+
/** Touch point: the wave starts there and leans that way. Pass the PointerEvent/MouseEvent. Keyboard clicks are ignored. */
|
|
23
|
+
focus?: MouseEvent | PointerEvent | KeyboardEvent | Point | null;
|
|
24
|
+
/** 'auto' picks from the source shape: wide → vertical, tall → horizontal, square or point → center. */
|
|
25
|
+
variant?: Variant;
|
|
26
|
+
/** A named palette or up to 8 hex colours. Interpolated in OKLab. */
|
|
27
|
+
palette?: PaletteName | string[];
|
|
28
|
+
/** Time multiplier (0.3 = slow motion). Default 1. */
|
|
29
|
+
speed?: number;
|
|
30
|
+
/** false to disable particles, or a particle count at reference scale. */
|
|
31
|
+
particles?: boolean | number;
|
|
32
|
+
/** Force reduced motion on/off. Default: follows prefers-reduced-motion. */
|
|
33
|
+
reducedMotion?: boolean | null;
|
|
34
|
+
/** Max ms to wait for the "after" capture before finishing without animation. Default 3000. */
|
|
35
|
+
timeout?: number;
|
|
36
|
+
/** Move keyboard focus to the equivalent control when the update replaces the focused node. Default true. */
|
|
37
|
+
preserveFocus?: boolean;
|
|
38
|
+
onLaunch?: () => void;
|
|
39
|
+
onCovered?: () => void;
|
|
40
|
+
onSettled?: () => void;
|
|
41
|
+
onEnd?: (metrics: Metrics) => void;
|
|
42
|
+
/** Any tuning key from the presets (amplitude, blur, rimWidth, lead, tail…), at reference scale. */
|
|
43
|
+
[tuning: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WaveTransition {
|
|
47
|
+
/** Resolves when the overlay is up and the animation started (or immediately when there is no animation). */
|
|
48
|
+
readonly ready: Promise<void>;
|
|
49
|
+
/** Resolves when your update callback finished. Rejects if it threw. */
|
|
50
|
+
readonly updateCallbackDone: Promise<void>;
|
|
51
|
+
/** Resolves when the wave has fully covered the target: safe to assume the old state is gone. */
|
|
52
|
+
readonly covered: Promise<void>;
|
|
53
|
+
/** Resolves when the overlay is gone and the real DOM is back. */
|
|
54
|
+
readonly finished: Promise<void>;
|
|
55
|
+
readonly metrics: Metrics;
|
|
56
|
+
/** Jump to the end. The update still runs. */
|
|
57
|
+
skip(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Plays a distortion wave on `target` while `update` changes the DOM. `update` always runs, even without WebGL. */
|
|
61
|
+
export function distortionWave(target: Element, update?: () => void | Promise<void>, options?: DistortionWaveOptions): WaveTransition;
|
|
62
|
+
|
|
63
|
+
/** Click `trigger` → wave on `target`, starting from `trigger` at the touch point. Pre-captures on hover/focus. */
|
|
64
|
+
export function waveOnClick(trigger: Element, target: Element, update: (event: MouseEvent) => void | Promise<void>, options?: DistortionWaveOptions): void;
|
|
65
|
+
|
|
66
|
+
/** Pre-captures `target` (e.g. on hover) to hide the "before" capture latency. Invalidated by any DOM mutation. */
|
|
67
|
+
export function prepareWave(target: Element): Promise<unknown>;
|
|
68
|
+
|
|
69
|
+
/** Creates a WebGL context and compiles the shaders ahead of the first interaction. Returns false without WebGL2. */
|
|
70
|
+
export function warmupWave(): boolean;
|
|
71
|
+
|
|
72
|
+
/** Rasterizes an element as currently rendered (SVG foreignObject). */
|
|
73
|
+
export function snapshotElement(el: Element, options?: { dpr?: number; clip?: { x: number; y: number; w: number; h: number }; rootOpacity?: string }): Promise<{
|
|
74
|
+
canvas: HTMLCanvasElement; w: number; h: number; dpr: number; ms: number; bytes: number;
|
|
75
|
+
}>;
|
|
76
|
+
|
|
77
|
+
export const PALETTES: Record<PaletteName, string[]>;
|
|
78
|
+
export const PRESETS: Record<'vertical' | 'horizontal' | 'center', Record<string, unknown>>;
|
|
79
|
+
export function scaleConfig(config: Record<string, unknown>, width: number, height: number): Record<string, unknown>;
|
|
80
|
+
export function totalDuration(config: Record<string, unknown>): number;
|
|
81
|
+
export function frameState(t: number, config: Record<string, unknown>, geometry: Record<string, unknown>): Record<string, unknown>;
|