@docmentis/udoc-viewer 0.6.26 → 0.6.27
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/README.md +4 -0
- package/dist/package.json +1 -1
- package/dist/src/UDocClient.d.ts +7 -0
- package/dist/src/UDocClient.d.ts.map +1 -1
- package/dist/src/UDocClient.js +5 -4
- package/dist/src/UDocClient.js.map +1 -1
- package/dist/src/UDocViewer.d.ts +1 -1
- package/dist/src/UDocViewer.d.ts.map +1 -1
- package/dist/src/UDocViewer.js +2 -2
- package/dist/src/UDocViewer.js.map +1 -1
- package/dist/src/ui/viewer/components/SubToolbar.d.ts.map +1 -1
- package/dist/src/ui/viewer/components/SubToolbar.js +9 -1
- package/dist/src/ui/viewer/components/SubToolbar.js.map +1 -1
- package/dist/src/ui/viewer/shell.d.ts +1 -1
- package/dist/src/ui/viewer/shell.d.ts.map +1 -1
- package/dist/src/ui/viewer/shell.js +4 -4
- package/dist/src/ui/viewer/shell.js.map +1 -1
- package/dist/src/ui/viewer/tools/AnnotationDrawController.d.ts.map +1 -1
- package/dist/src/ui/viewer/tools/AnnotationDrawController.js +13 -5
- package/dist/src/ui/viewer/tools/AnnotationDrawController.js.map +1 -1
- package/dist/src/ui/viewer/transition-gl.d.ts +25 -0
- package/dist/src/ui/viewer/transition-gl.d.ts.map +1 -0
- package/dist/src/ui/viewer/transition-gl.js +823 -0
- package/dist/src/ui/viewer/transition-gl.js.map +1 -0
- package/dist/src/ui/viewer/transition.d.ts.map +1 -1
- package/dist/src/ui/viewer/transition.js +9 -0
- package/dist/src/ui/viewer/transition.js.map +1 -1
- package/dist/src/wasm/udoc.d.ts +1 -1
- package/dist/src/wasm/udoc.js +3 -3
- package/dist/src/wasm/udoc_bg.wasm +0 -0
- package/dist/src/wasm/udoc_bg.wasm.d.ts +1 -1
- package/dist/src/worker/worker-inline.js +1 -1
- package/dist/src/worker/worker.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGL2-accelerated slide transitions.
|
|
3
|
+
*
|
|
4
|
+
* Currently implements: vortex.
|
|
5
|
+
*
|
|
6
|
+
* The entry point is tryRunGLTransition(), called from runTransition() in
|
|
7
|
+
* transition.ts BEFORE the CSS path runs. It returns null when the effect
|
|
8
|
+
* isn't GL-backed or when GL setup fails — in that case the caller falls
|
|
9
|
+
* back to the CSS path and there is no visible regression.
|
|
10
|
+
*
|
|
11
|
+
* Same DOM contract as the CSS path (see transition.ts header):
|
|
12
|
+
* - The outgoing element (snapshot overlay) is disposable — we mutate
|
|
13
|
+
* it freely (innerHTML, zIndex, a GL <canvas> child).
|
|
14
|
+
* - The incoming element (real spread) is only touched via `opacity`.
|
|
15
|
+
* NEVER set `transform` on incoming.
|
|
16
|
+
*/
|
|
17
|
+
// 4x the tile count of the CSS fallback (40x22). The GL path pays one draw
|
|
18
|
+
// call per phase regardless of tile count, so we can afford a much finer
|
|
19
|
+
// grid for a smoother, more detailed vortex.
|
|
20
|
+
const VORTEX_COLS = 80;
|
|
21
|
+
const VORTEX_ROWS = 44;
|
|
22
|
+
/**
|
|
23
|
+
* Try to run a GL-accelerated transition. Returns null if the effect is
|
|
24
|
+
* not GL-backed, WebGL2 isn't available, or texture setup fails — caller
|
|
25
|
+
* should fall back to the CSS path.
|
|
26
|
+
*/
|
|
27
|
+
export function tryRunGLTransition(outgoing, incoming, transition, _forward, durationMs, onComplete) {
|
|
28
|
+
const effect = transition.effect;
|
|
29
|
+
if (effect.type === "vortex") {
|
|
30
|
+
return runVortex(outgoing, incoming, effect.direction, durationMs, onComplete);
|
|
31
|
+
}
|
|
32
|
+
if (effect.type === "switch") {
|
|
33
|
+
return runSwitch(outgoing, incoming, effect.direction, durationMs, onComplete);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Vortex
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
function runVortex(outgoing, incoming, dir, durationMs, onComplete) {
|
|
41
|
+
const outCanvas = outgoing.querySelector("canvas");
|
|
42
|
+
const inCanvas = incoming.querySelector(".udoc-spread__canvas") ??
|
|
43
|
+
incoming.querySelector("canvas");
|
|
44
|
+
if (!outCanvas || !inCanvas || outCanvas.width === 0 || inCanvas.width === 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const dpr = window.devicePixelRatio || 1;
|
|
48
|
+
const slideW = outCanvas.width / dpr;
|
|
49
|
+
const slideH = outCanvas.height / dpr;
|
|
50
|
+
// Detached GL canvas. Everything up to the "Commit" line below is
|
|
51
|
+
// side-effect-free from the page's perspective — if any step fails
|
|
52
|
+
// we can return null and the CSS fallback runs cleanly.
|
|
53
|
+
const glCanvas = document.createElement("canvas");
|
|
54
|
+
glCanvas.width = outCanvas.width;
|
|
55
|
+
glCanvas.height = outCanvas.height;
|
|
56
|
+
const gl = glCanvas.getContext("webgl2", {
|
|
57
|
+
alpha: false,
|
|
58
|
+
antialias: true,
|
|
59
|
+
depth: true,
|
|
60
|
+
premultipliedAlpha: false,
|
|
61
|
+
});
|
|
62
|
+
if (!gl)
|
|
63
|
+
return null;
|
|
64
|
+
const maxTex = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
65
|
+
if (outCanvas.width > maxTex || outCanvas.height > maxTex || inCanvas.width > maxTex || inCanvas.height > maxTex) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const program = createProgram(gl, VORTEX_VS, VORTEX_FS);
|
|
69
|
+
if (!program)
|
|
70
|
+
return null;
|
|
71
|
+
const outTex = uploadTexture(gl, outCanvas);
|
|
72
|
+
const inTex = uploadTexture(gl, inCanvas);
|
|
73
|
+
if (!outTex || !inTex) {
|
|
74
|
+
if (outTex)
|
|
75
|
+
gl.deleteTexture(outTex);
|
|
76
|
+
if (inTex)
|
|
77
|
+
gl.deleteTexture(inTex);
|
|
78
|
+
gl.deleteProgram(program);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const mesh = buildVortexMesh(VORTEX_COLS, VORTEX_ROWS, slideW, slideH, dir);
|
|
82
|
+
const vbo = gl.createBuffer();
|
|
83
|
+
if (!vbo) {
|
|
84
|
+
gl.deleteTexture(outTex);
|
|
85
|
+
gl.deleteTexture(inTex);
|
|
86
|
+
gl.deleteProgram(program);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
90
|
+
gl.bufferData(gl.ARRAY_BUFFER, mesh.data, gl.STATIC_DRAW);
|
|
91
|
+
gl.useProgram(program);
|
|
92
|
+
const F = 4; // bytes per float
|
|
93
|
+
const STRIDE = 11 * F;
|
|
94
|
+
bindAttrib(gl, program, "a_localPos", 2, STRIDE, 0 * F);
|
|
95
|
+
bindAttrib(gl, program, "a_tileCenter", 2, STRIDE, 2 * F);
|
|
96
|
+
bindAttrib(gl, program, "a_uv", 2, STRIDE, 4 * F);
|
|
97
|
+
bindAttrib(gl, program, "a_staggerOut", 1, STRIDE, 6 * F);
|
|
98
|
+
bindAttrib(gl, program, "a_staggerIn", 1, STRIDE, 7 * F);
|
|
99
|
+
bindAttrib(gl, program, "a_jitter", 3, STRIDE, 8 * F);
|
|
100
|
+
const uT = gl.getUniformLocation(program, "u_t");
|
|
101
|
+
const uResolution = gl.getUniformLocation(program, "u_resolution");
|
|
102
|
+
const uIsHorizontal = gl.getUniformLocation(program, "u_isHorizontal");
|
|
103
|
+
const uRotSign = gl.getUniformLocation(program, "u_rotSign");
|
|
104
|
+
const uPhase = gl.getUniformLocation(program, "u_phase");
|
|
105
|
+
const uTex = gl.getUniformLocation(program, "u_tex");
|
|
106
|
+
const uPerspective = gl.getUniformLocation(program, "u_perspective");
|
|
107
|
+
const isH = dir === "left" || dir === "right";
|
|
108
|
+
const rotSign = dir === "right" || dir === "up" ? 1 : -1;
|
|
109
|
+
gl.uniform2f(uResolution, slideW, slideH);
|
|
110
|
+
gl.uniform1f(uIsHorizontal, isH ? 1 : 0);
|
|
111
|
+
gl.uniform1f(uRotSign, rotSign);
|
|
112
|
+
gl.uniform1f(uPerspective, Math.max(slideW, slideH) * 2);
|
|
113
|
+
gl.uniform1i(uTex, 0);
|
|
114
|
+
gl.viewport(0, 0, glCanvas.width, glCanvas.height);
|
|
115
|
+
gl.enable(gl.DEPTH_TEST);
|
|
116
|
+
gl.depthFunc(gl.LEQUAL);
|
|
117
|
+
gl.clearColor(0, 0, 0, 1);
|
|
118
|
+
gl.clearDepth(1);
|
|
119
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
120
|
+
// ---- All fallible GL setup succeeded. Commit DOM mutations. ----
|
|
121
|
+
glCanvas.style.position = "absolute";
|
|
122
|
+
glCanvas.style.left = "0";
|
|
123
|
+
glCanvas.style.top = "0";
|
|
124
|
+
glCanvas.style.width = `${slideW}px`;
|
|
125
|
+
glCanvas.style.height = `${slideH}px`;
|
|
126
|
+
glCanvas.style.display = "block";
|
|
127
|
+
outgoing.innerHTML = "";
|
|
128
|
+
outgoing.style.overflow = "visible";
|
|
129
|
+
outgoing.style.zIndex = "1";
|
|
130
|
+
outgoing.style.pointerEvents = "none";
|
|
131
|
+
outgoing.appendChild(glCanvas);
|
|
132
|
+
incoming.style.opacity = "0";
|
|
133
|
+
// Render the first frame synchronously so the GL canvas has content
|
|
134
|
+
// before the browser composites — avoids a single-frame flash of black.
|
|
135
|
+
drawFrame(0);
|
|
136
|
+
let rafId = 0;
|
|
137
|
+
let done = false;
|
|
138
|
+
const startTime = performance.now();
|
|
139
|
+
function drawFrame(t) {
|
|
140
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
141
|
+
gl.uniform1f(uT, t);
|
|
142
|
+
// Phase 0 — outgoing tiles
|
|
143
|
+
gl.uniform1f(uPhase, 0);
|
|
144
|
+
gl.bindTexture(gl.TEXTURE_2D, outTex);
|
|
145
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount);
|
|
146
|
+
// Phase 1 — incoming tiles
|
|
147
|
+
gl.uniform1f(uPhase, 1);
|
|
148
|
+
gl.bindTexture(gl.TEXTURE_2D, inTex);
|
|
149
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount);
|
|
150
|
+
}
|
|
151
|
+
function tick(now) {
|
|
152
|
+
if (done)
|
|
153
|
+
return;
|
|
154
|
+
const elapsed = now - startTime;
|
|
155
|
+
const raw = Math.min(elapsed / durationMs, 1);
|
|
156
|
+
const eased = easeInOut(raw);
|
|
157
|
+
drawFrame(eased);
|
|
158
|
+
if (raw < 1) {
|
|
159
|
+
rafId = requestAnimationFrame(tick);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
finish();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function finish() {
|
|
166
|
+
if (done)
|
|
167
|
+
return;
|
|
168
|
+
done = true;
|
|
169
|
+
cancelAnimationFrame(rafId);
|
|
170
|
+
// Tear down GL — wrapped in try/catch because the context may have
|
|
171
|
+
// been lost (or already released by loseContext() below).
|
|
172
|
+
try {
|
|
173
|
+
gl.deleteTexture(outTex);
|
|
174
|
+
gl.deleteTexture(inTex);
|
|
175
|
+
gl.deleteBuffer(vbo);
|
|
176
|
+
gl.deleteProgram(program);
|
|
177
|
+
const lose = gl.getExtension("WEBGL_lose_context");
|
|
178
|
+
if (lose)
|
|
179
|
+
lose.loseContext();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// ignore
|
|
183
|
+
}
|
|
184
|
+
// Restore incoming — outgoing is about to be removed by the caller.
|
|
185
|
+
incoming.style.opacity = "";
|
|
186
|
+
incoming.style.clipPath = "";
|
|
187
|
+
onComplete();
|
|
188
|
+
}
|
|
189
|
+
glCanvas.addEventListener("webglcontextlost", (e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
finish();
|
|
192
|
+
});
|
|
193
|
+
rafId = requestAnimationFrame(tick);
|
|
194
|
+
return { cancel: finish };
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Shaders
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Vortex vertex shader.
|
|
200
|
+
//
|
|
201
|
+
// Matches the CSS vortex math from transition.ts: each tile rotates around
|
|
202
|
+
// a line through the slide center (vertical axis for horizontal direction,
|
|
203
|
+
// horizontal axis for vertical direction), with per-tile Z bump and
|
|
204
|
+
// perpendicular drift modulated by sin(πt). Per-tile stagger is supplied as
|
|
205
|
+
// a vertex attribute so the sweep animates without CPU per-frame work.
|
|
206
|
+
//
|
|
207
|
+
// CSS-style perspective divide is applied manually around the slide center
|
|
208
|
+
// so the result matches the existing effect's projection.
|
|
209
|
+
const VORTEX_VS = `#version 300 es
|
|
210
|
+
precision highp float;
|
|
211
|
+
|
|
212
|
+
in vec2 a_localPos; // vertex xy relative to tile center, in slide pixels
|
|
213
|
+
in vec2 a_tileCenter; // tile center in slide pixels
|
|
214
|
+
in vec2 a_uv; // texture coord (0..1, origin top-left)
|
|
215
|
+
in float a_staggerOut;
|
|
216
|
+
in float a_staggerIn;
|
|
217
|
+
in vec3 a_jitter; // (zSign, zRand, driftRand)
|
|
218
|
+
|
|
219
|
+
uniform float u_t; // eased animation time 0..1
|
|
220
|
+
uniform vec2 u_resolution; // slide width/height in CSS pixels
|
|
221
|
+
uniform float u_isHorizontal; // 1 for left/right, 0 for up/down
|
|
222
|
+
uniform float u_rotSign; // +1 or -1
|
|
223
|
+
uniform float u_phase; // 0 = outgoing, 1 = incoming
|
|
224
|
+
uniform float u_perspective; // CSS perspective, in pixels
|
|
225
|
+
|
|
226
|
+
out vec2 v_uv;
|
|
227
|
+
out float v_facing;
|
|
228
|
+
|
|
229
|
+
const float PI = 3.14159265;
|
|
230
|
+
|
|
231
|
+
// Rotation axis is pushed back into the screen by this fraction of the
|
|
232
|
+
// slide's longest side. A larger value means a bigger orbit radius — tiles
|
|
233
|
+
// arc visibly backward through the screen plane instead of only sliding
|
|
234
|
+
// sideways. The CSS fallback uses 0 (axis on the slide plane itself).
|
|
235
|
+
const float AXIS_DEPTH_FACTOR = 0.5;
|
|
236
|
+
|
|
237
|
+
float clamp01(float x) { return clamp(x, 0.0, 1.0); }
|
|
238
|
+
|
|
239
|
+
void main() {
|
|
240
|
+
vec2 center = u_resolution * 0.5;
|
|
241
|
+
vec2 rest = a_tileCenter + a_localPos;
|
|
242
|
+
float axisDepth = max(u_resolution.x, u_resolution.y) * AXIS_DEPTH_FACTOR;
|
|
243
|
+
|
|
244
|
+
float stagger = (u_phase < 0.5) ? a_staggerOut : a_staggerIn;
|
|
245
|
+
float offset = (u_phase < 0.5) ? 0.0 : 0.1;
|
|
246
|
+
float localT = clamp01((u_t - offset - stagger * 0.4) / 0.55);
|
|
247
|
+
|
|
248
|
+
float baseDeg = (u_phase < 0.5)
|
|
249
|
+
? (localT * 180.0)
|
|
250
|
+
: (-180.0 + localT * 180.0);
|
|
251
|
+
float angle = radians(u_rotSign * baseDeg);
|
|
252
|
+
|
|
253
|
+
// A tile shows its front while |rotation| < 90°.
|
|
254
|
+
// Outgoing: visible for localT < 0.5
|
|
255
|
+
// Incoming: visible for localT > 0.5
|
|
256
|
+
v_facing = (u_phase < 0.5) ? step(localT, 0.5) : step(0.5, localT);
|
|
257
|
+
|
|
258
|
+
float zSign = a_jitter.x;
|
|
259
|
+
float zRand = a_jitter.y;
|
|
260
|
+
float driftRand = a_jitter.z;
|
|
261
|
+
float sinPi = sin(localT * PI);
|
|
262
|
+
float zOffset = zSign * sinPi * 200.0 * zRand;
|
|
263
|
+
|
|
264
|
+
// Drift is perpendicular to the rotation axis.
|
|
265
|
+
vec2 d = center - a_tileCenter;
|
|
266
|
+
float dPerp = (u_isHorizontal > 0.5) ? d.y : d.x;
|
|
267
|
+
float drift = sinPi * dPerp * 0.5 * driftRand;
|
|
268
|
+
|
|
269
|
+
float c = cos(angle);
|
|
270
|
+
float s = sin(angle);
|
|
271
|
+
|
|
272
|
+
// 3D orbit around an axis pushed back into the screen by axisDepth.
|
|
273
|
+
//
|
|
274
|
+
// Each vertex, relative to the axis, starts at offset (u, axisDepth)
|
|
275
|
+
// in the rotation plane (xz for horizontal, yz for vertical). After
|
|
276
|
+
// rotating by the angle, its new in-plane offset is:
|
|
277
|
+
// u' = c*u + s*axisDepth
|
|
278
|
+
// z' = -s*u + c*axisDepth
|
|
279
|
+
// Converting back to world coords (axis sits at z = -axisDepth) gives
|
|
280
|
+
// the formulas below. At angle=0 every vertex is at rest; at 180° the
|
|
281
|
+
// tile is mirrored across the axis AND pushed to z = -2*axisDepth —
|
|
282
|
+
// a real semicircular arc rather than an in-plane flip.
|
|
283
|
+
vec3 world;
|
|
284
|
+
if (u_isHorizontal > 0.5) {
|
|
285
|
+
float u = rest.x - center.x;
|
|
286
|
+
world.x = center.x + c * u + s * axisDepth;
|
|
287
|
+
world.y = rest.y + drift;
|
|
288
|
+
world.z = -s * u + axisDepth * (c - 1.0) + zOffset;
|
|
289
|
+
} else {
|
|
290
|
+
float v = rest.y - center.y;
|
|
291
|
+
world.x = rest.x + drift;
|
|
292
|
+
world.y = center.y + c * v - s * axisDepth;
|
|
293
|
+
world.z = s * v + axisDepth * (c - 1.0) + zOffset;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// CSS-style perspective: camera at z = +P, perspective-origin = center.
|
|
297
|
+
float wp = max(1.0 - world.z / u_perspective, 0.001);
|
|
298
|
+
vec2 screen = center + (world.xy - center) / wp;
|
|
299
|
+
|
|
300
|
+
// Slide pixels → clip space. Screen uses +y down; clip is +y up.
|
|
301
|
+
float clipX = (screen.x / u_resolution.x) * 2.0 - 1.0;
|
|
302
|
+
float clipY = 1.0 - (screen.y / u_resolution.y) * 2.0;
|
|
303
|
+
// Preserve z for depth testing so overlapping tiles sort correctly.
|
|
304
|
+
float clipZ = clamp(-world.z / u_perspective, -0.99, 0.99);
|
|
305
|
+
|
|
306
|
+
gl_Position = vec4(clipX, clipY, clipZ, 1.0);
|
|
307
|
+
v_uv = a_uv;
|
|
308
|
+
}
|
|
309
|
+
`;
|
|
310
|
+
const VORTEX_FS = `#version 300 es
|
|
311
|
+
precision highp float;
|
|
312
|
+
|
|
313
|
+
in vec2 v_uv;
|
|
314
|
+
in float v_facing;
|
|
315
|
+
|
|
316
|
+
uniform sampler2D u_tex;
|
|
317
|
+
|
|
318
|
+
out vec4 outColor;
|
|
319
|
+
|
|
320
|
+
void main() {
|
|
321
|
+
// Backface hide without relying on winding/cull, so the shader works
|
|
322
|
+
// regardless of y-flip conventions.
|
|
323
|
+
if (v_facing < 0.5) discard;
|
|
324
|
+
outColor = texture(u_tex, v_uv);
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Switch
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
//
|
|
331
|
+
// Visual model (dir="left"):
|
|
332
|
+
//
|
|
333
|
+
// Two sheets of paper stacked at z≈0, both facing the viewer. Paper 1
|
|
334
|
+
// (outgoing) is slightly in front, paper 2 (incoming) slightly behind and
|
|
335
|
+
// hidden. Left hand grabs paper 1 at its LEFT edge; right hand grabs
|
|
336
|
+
// paper 2 at its RIGHT edge. Both hands move apart while each paper
|
|
337
|
+
// rotates outward around its own hinge — the free edges swing forward
|
|
338
|
+
// toward the viewer, opening like two cabinet doors. At the midpoint
|
|
339
|
+
// the papers no longer touch. Then everything reverses and the stack
|
|
340
|
+
// closes back up — but by then paper 2 has taken paper 1's place on top.
|
|
341
|
+
//
|
|
342
|
+
// The "swap" is invisible during the motion because the papers are rotated
|
|
343
|
+
// apart. We implement it as a linear z-interpolation on each paper's hinge
|
|
344
|
+
// base-z: paper 1 drifts from +Z_SEP to -Z_SEP, paper 2 from -Z_SEP to
|
|
345
|
+
// +Z_SEP. With depth test enabled the nearer paper wins at every frame.
|
|
346
|
+
//
|
|
347
|
+
// Parameters (all tunable below): MAX_ANGLE is how far the papers open,
|
|
348
|
+
// HAND_SHIFT is how far the hinges translate outward, Z_SEP is just the
|
|
349
|
+
// tiny depth offset needed to break the initial/final ties.
|
|
350
|
+
// Keyframes (hinge position + rotation), using paper-local coordinate signs:
|
|
351
|
+
//
|
|
352
|
+
// Left page (hinge = left edge):
|
|
353
|
+
// t=0.0 x=0, z=0, rot=0
|
|
354
|
+
// t=0.5 x=-50, z=10, rot=+30°
|
|
355
|
+
// t=1.0 x=0, z=20, rot=0
|
|
356
|
+
//
|
|
357
|
+
// Right page (hinge = right edge):
|
|
358
|
+
// t=0.0 x=W, z=20, rot=0
|
|
359
|
+
// t=0.5 x=W+50,z=10, rot=-30°
|
|
360
|
+
// t=1.0 x=W, z=0, rot=0
|
|
361
|
+
//
|
|
362
|
+
// Interpolation: x and rotation follow sin(πt) (peak at mid, zero at ends).
|
|
363
|
+
// z is linear monotonic — the two pages swap depth over the animation, so
|
|
364
|
+
// paper 1 ends up behind paper 2 by t=1.
|
|
365
|
+
const SWITCH_MAX_ANGLE_RAD = (Math.PI / 180) * 45;
|
|
366
|
+
const SWITCH_HAND_SHIFT_PX = 200;
|
|
367
|
+
const SWITCH_Z_MAX = 350;
|
|
368
|
+
function computeSwitchGeometry(dir, slideW, slideH) {
|
|
369
|
+
// Rotation signs are chosen so each paper's free edge rotates BACKWARD
|
|
370
|
+
// (into the screen, away from the viewer). Sibling papers open like two
|
|
371
|
+
// panels falling away from each other into a cavity behind the plane.
|
|
372
|
+
switch (dir) {
|
|
373
|
+
case "left":
|
|
374
|
+
return {
|
|
375
|
+
p1HingeLocal: [0, 0],
|
|
376
|
+
p2HingeLocal: [slideW, 0],
|
|
377
|
+
p1RotSign: +1,
|
|
378
|
+
p2RotSign: -1,
|
|
379
|
+
p1Shift: [-1, 0],
|
|
380
|
+
p2Shift: [+1, 0],
|
|
381
|
+
};
|
|
382
|
+
case "right":
|
|
383
|
+
return {
|
|
384
|
+
p1HingeLocal: [slideW, 0],
|
|
385
|
+
p2HingeLocal: [0, 0],
|
|
386
|
+
p1RotSign: -1,
|
|
387
|
+
p2RotSign: +1,
|
|
388
|
+
p1Shift: [+1, 0],
|
|
389
|
+
p2Shift: [-1, 0],
|
|
390
|
+
};
|
|
391
|
+
case "up":
|
|
392
|
+
return {
|
|
393
|
+
p1HingeLocal: [0, 0],
|
|
394
|
+
p2HingeLocal: [0, slideH],
|
|
395
|
+
p1RotSign: -1,
|
|
396
|
+
p2RotSign: +1,
|
|
397
|
+
p1Shift: [0, -1],
|
|
398
|
+
p2Shift: [0, +1],
|
|
399
|
+
};
|
|
400
|
+
case "down":
|
|
401
|
+
return {
|
|
402
|
+
p1HingeLocal: [0, slideH],
|
|
403
|
+
p2HingeLocal: [0, 0],
|
|
404
|
+
p1RotSign: +1,
|
|
405
|
+
p2RotSign: -1,
|
|
406
|
+
p1Shift: [0, +1],
|
|
407
|
+
p2Shift: [0, -1],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function runSwitch(outgoing, incoming, dir, durationMs, onComplete) {
|
|
412
|
+
const outCanvas = outgoing.querySelector("canvas");
|
|
413
|
+
const inCanvas = incoming.querySelector(".udoc-spread__canvas") ??
|
|
414
|
+
incoming.querySelector("canvas");
|
|
415
|
+
if (!outCanvas || !inCanvas || outCanvas.width === 0 || inCanvas.width === 0) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const dpr = window.devicePixelRatio || 1;
|
|
419
|
+
const slideW = outCanvas.width / dpr;
|
|
420
|
+
const slideH = outCanvas.height / dpr;
|
|
421
|
+
const glCanvas = document.createElement("canvas");
|
|
422
|
+
glCanvas.width = outCanvas.width;
|
|
423
|
+
glCanvas.height = outCanvas.height;
|
|
424
|
+
const gl = glCanvas.getContext("webgl2", {
|
|
425
|
+
alpha: false,
|
|
426
|
+
antialias: true,
|
|
427
|
+
depth: true,
|
|
428
|
+
premultipliedAlpha: false,
|
|
429
|
+
});
|
|
430
|
+
if (!gl)
|
|
431
|
+
return null;
|
|
432
|
+
const maxTex = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
433
|
+
if (outCanvas.width > maxTex || outCanvas.height > maxTex || inCanvas.width > maxTex || inCanvas.height > maxTex) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const program = createProgram(gl, SWITCH_VS, SWITCH_FS);
|
|
437
|
+
if (!program)
|
|
438
|
+
return null;
|
|
439
|
+
const outTex = uploadTexture(gl, outCanvas);
|
|
440
|
+
const inTex = uploadTexture(gl, inCanvas);
|
|
441
|
+
if (!outTex || !inTex) {
|
|
442
|
+
if (outTex)
|
|
443
|
+
gl.deleteTexture(outTex);
|
|
444
|
+
if (inTex)
|
|
445
|
+
gl.deleteTexture(inTex);
|
|
446
|
+
gl.deleteProgram(program);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
// A single slide-sized quad shared by both papers (4 floats per vert:
|
|
450
|
+
// localX, localY, u, v). 2 triangles, 6 verts total.
|
|
451
|
+
const meshData = new Float32Array([
|
|
452
|
+
0,
|
|
453
|
+
0,
|
|
454
|
+
0,
|
|
455
|
+
0,
|
|
456
|
+
slideW,
|
|
457
|
+
0,
|
|
458
|
+
1,
|
|
459
|
+
0,
|
|
460
|
+
slideW,
|
|
461
|
+
slideH,
|
|
462
|
+
1,
|
|
463
|
+
1,
|
|
464
|
+
0,
|
|
465
|
+
0,
|
|
466
|
+
0,
|
|
467
|
+
0,
|
|
468
|
+
slideW,
|
|
469
|
+
slideH,
|
|
470
|
+
1,
|
|
471
|
+
1,
|
|
472
|
+
0,
|
|
473
|
+
slideH,
|
|
474
|
+
0,
|
|
475
|
+
1,
|
|
476
|
+
]);
|
|
477
|
+
const vertexCount = 6;
|
|
478
|
+
const vbo = gl.createBuffer();
|
|
479
|
+
if (!vbo) {
|
|
480
|
+
gl.deleteTexture(outTex);
|
|
481
|
+
gl.deleteTexture(inTex);
|
|
482
|
+
gl.deleteProgram(program);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
486
|
+
gl.bufferData(gl.ARRAY_BUFFER, meshData, gl.STATIC_DRAW);
|
|
487
|
+
gl.useProgram(program);
|
|
488
|
+
const F = 4;
|
|
489
|
+
const STRIDE = 4 * F;
|
|
490
|
+
bindAttrib(gl, program, "a_localPos", 2, STRIDE, 0 * F);
|
|
491
|
+
bindAttrib(gl, program, "a_uv", 2, STRIDE, 2 * F);
|
|
492
|
+
const uResolution = gl.getUniformLocation(program, "u_resolution");
|
|
493
|
+
const uPerspective = gl.getUniformLocation(program, "u_perspective");
|
|
494
|
+
const uIsHorizontal = gl.getUniformLocation(program, "u_isHorizontal");
|
|
495
|
+
const uHingeWorld = gl.getUniformLocation(program, "u_hingeWorld");
|
|
496
|
+
const uHingeLocal = gl.getUniformLocation(program, "u_hingeLocal");
|
|
497
|
+
const uAngle = gl.getUniformLocation(program, "u_angle");
|
|
498
|
+
const uTex = gl.getUniformLocation(program, "u_tex");
|
|
499
|
+
const isH = dir === "left" || dir === "right";
|
|
500
|
+
const maxDim = Math.max(slideW, slideH);
|
|
501
|
+
const geom = computeSwitchGeometry(dir, slideW, slideH);
|
|
502
|
+
gl.uniform2f(uResolution, slideW, slideH);
|
|
503
|
+
gl.uniform1f(uPerspective, maxDim * 2);
|
|
504
|
+
gl.uniform1f(uIsHorizontal, isH ? 1 : 0);
|
|
505
|
+
gl.uniform1i(uTex, 0);
|
|
506
|
+
gl.viewport(0, 0, glCanvas.width, glCanvas.height);
|
|
507
|
+
gl.enable(gl.DEPTH_TEST);
|
|
508
|
+
gl.depthFunc(gl.LEQUAL);
|
|
509
|
+
gl.clearColor(0, 0, 0, 1);
|
|
510
|
+
gl.clearDepth(1);
|
|
511
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
512
|
+
// ---- Commit DOM mutations ----
|
|
513
|
+
glCanvas.style.position = "absolute";
|
|
514
|
+
glCanvas.style.left = "0";
|
|
515
|
+
glCanvas.style.top = "0";
|
|
516
|
+
glCanvas.style.width = `${slideW}px`;
|
|
517
|
+
glCanvas.style.height = `${slideH}px`;
|
|
518
|
+
glCanvas.style.display = "block";
|
|
519
|
+
outgoing.innerHTML = "";
|
|
520
|
+
outgoing.style.overflow = "visible";
|
|
521
|
+
outgoing.style.zIndex = "1";
|
|
522
|
+
outgoing.style.pointerEvents = "none";
|
|
523
|
+
outgoing.appendChild(glCanvas);
|
|
524
|
+
incoming.style.opacity = "0";
|
|
525
|
+
function drawFrame(t) {
|
|
526
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
527
|
+
const sinPiT = Math.sin(Math.PI * t);
|
|
528
|
+
const theta = SWITCH_MAX_ANGLE_RAD * sinPiT;
|
|
529
|
+
const shift = SWITCH_HAND_SHIFT_PX * sinPiT;
|
|
530
|
+
// Linear z swap between the two pages:
|
|
531
|
+
// paper 1: 0 → -SWITCH_Z_MAX (moves back)
|
|
532
|
+
// paper 2: -SWITCH_Z_MAX → 0 (moves forward)
|
|
533
|
+
const p1BaseZ = -SWITCH_Z_MAX * t;
|
|
534
|
+
const p2BaseZ = -SWITCH_Z_MAX * (1 - t);
|
|
535
|
+
const p1Wx = geom.p1HingeLocal[0] + geom.p1Shift[0] * shift;
|
|
536
|
+
const p1Wy = geom.p1HingeLocal[1] + geom.p1Shift[1] * shift;
|
|
537
|
+
const p2Wx = geom.p2HingeLocal[0] + geom.p2Shift[0] * shift;
|
|
538
|
+
const p2Wy = geom.p2HingeLocal[1] + geom.p2Shift[1] * shift;
|
|
539
|
+
// Paper 1 (outgoing)
|
|
540
|
+
gl.uniform3f(uHingeWorld, p1Wx, p1Wy, p1BaseZ);
|
|
541
|
+
gl.uniform2f(uHingeLocal, geom.p1HingeLocal[0], geom.p1HingeLocal[1]);
|
|
542
|
+
gl.uniform1f(uAngle, geom.p1RotSign * theta);
|
|
543
|
+
gl.bindTexture(gl.TEXTURE_2D, outTex);
|
|
544
|
+
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
|
|
545
|
+
// Paper 2 (incoming)
|
|
546
|
+
gl.uniform3f(uHingeWorld, p2Wx, p2Wy, p2BaseZ);
|
|
547
|
+
gl.uniform2f(uHingeLocal, geom.p2HingeLocal[0], geom.p2HingeLocal[1]);
|
|
548
|
+
gl.uniform1f(uAngle, geom.p2RotSign * theta);
|
|
549
|
+
gl.bindTexture(gl.TEXTURE_2D, inTex);
|
|
550
|
+
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
|
|
551
|
+
}
|
|
552
|
+
drawFrame(0);
|
|
553
|
+
let rafId = 0;
|
|
554
|
+
let done = false;
|
|
555
|
+
const startTime = performance.now();
|
|
556
|
+
function tick(now) {
|
|
557
|
+
if (done)
|
|
558
|
+
return;
|
|
559
|
+
const elapsed = now - startTime;
|
|
560
|
+
const raw = Math.min(elapsed / durationMs, 1);
|
|
561
|
+
const eased = easeInOut(raw);
|
|
562
|
+
drawFrame(eased);
|
|
563
|
+
if (raw < 1) {
|
|
564
|
+
rafId = requestAnimationFrame(tick);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
finish();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function finish() {
|
|
571
|
+
if (done)
|
|
572
|
+
return;
|
|
573
|
+
done = true;
|
|
574
|
+
cancelAnimationFrame(rafId);
|
|
575
|
+
try {
|
|
576
|
+
gl.deleteTexture(outTex);
|
|
577
|
+
gl.deleteTexture(inTex);
|
|
578
|
+
gl.deleteBuffer(vbo);
|
|
579
|
+
gl.deleteProgram(program);
|
|
580
|
+
const lose = gl.getExtension("WEBGL_lose_context");
|
|
581
|
+
if (lose)
|
|
582
|
+
lose.loseContext();
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// ignore
|
|
586
|
+
}
|
|
587
|
+
incoming.style.opacity = "";
|
|
588
|
+
incoming.style.clipPath = "";
|
|
589
|
+
onComplete();
|
|
590
|
+
}
|
|
591
|
+
glCanvas.addEventListener("webglcontextlost", (e) => {
|
|
592
|
+
e.preventDefault();
|
|
593
|
+
finish();
|
|
594
|
+
});
|
|
595
|
+
rafId = requestAnimationFrame(tick);
|
|
596
|
+
return { cancel: finish };
|
|
597
|
+
}
|
|
598
|
+
// Switch vertex shader: each paper is rigid-rotated around its hinge line
|
|
599
|
+
// (vertical for horizontal directions, horizontal for vertical directions).
|
|
600
|
+
// Per vertex: translate into hinge-local space, apply the rotation, add
|
|
601
|
+
// the hinge's current world position, then apply a CSS-style perspective
|
|
602
|
+
// divide so the projection matches the vortex shader.
|
|
603
|
+
const SWITCH_VS = `#version 300 es
|
|
604
|
+
precision highp float;
|
|
605
|
+
|
|
606
|
+
in vec2 a_localPos;
|
|
607
|
+
in vec2 a_uv;
|
|
608
|
+
|
|
609
|
+
uniform vec2 u_resolution;
|
|
610
|
+
uniform float u_perspective;
|
|
611
|
+
uniform float u_isHorizontal;
|
|
612
|
+
uniform vec3 u_hingeWorld;
|
|
613
|
+
uniform vec2 u_hingeLocal;
|
|
614
|
+
uniform float u_angle;
|
|
615
|
+
|
|
616
|
+
out vec2 v_uv;
|
|
617
|
+
|
|
618
|
+
void main() {
|
|
619
|
+
vec2 rel = a_localPos - u_hingeLocal;
|
|
620
|
+
|
|
621
|
+
float c = cos(u_angle);
|
|
622
|
+
float s = sin(u_angle);
|
|
623
|
+
|
|
624
|
+
vec3 rotated;
|
|
625
|
+
if (u_isHorizontal > 0.5) {
|
|
626
|
+
// Rotate around Y axis: x' = c*x, z' = -s*x (initial z = 0)
|
|
627
|
+
rotated = vec3(c * rel.x, rel.y, -s * rel.x);
|
|
628
|
+
} else {
|
|
629
|
+
// Rotate around X axis: y' = c*y, z' = s*y (initial z = 0)
|
|
630
|
+
rotated = vec3(rel.x, c * rel.y, s * rel.y);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
vec3 world = u_hingeWorld + rotated;
|
|
634
|
+
|
|
635
|
+
// CSS-style perspective divide around slide center.
|
|
636
|
+
vec2 center = u_resolution * 0.5;
|
|
637
|
+
float wp = max(1.0 - world.z / u_perspective, 0.001);
|
|
638
|
+
vec2 screen = center + (world.xy - center) / wp;
|
|
639
|
+
|
|
640
|
+
float clipX = (screen.x / u_resolution.x) * 2.0 - 1.0;
|
|
641
|
+
float clipY = 1.0 - (screen.y / u_resolution.y) * 2.0;
|
|
642
|
+
float clipZ = clamp(-world.z / u_perspective, -0.99, 0.99);
|
|
643
|
+
|
|
644
|
+
// Output in full homogeneous form so GL's own perspective divide
|
|
645
|
+
// undoes the *wp multiply and, crucially, uses 1/w for
|
|
646
|
+
// perspective-correct interpolation of v_uv across the quad's two
|
|
647
|
+
// triangles. Outputting w=1 (pre-divided) makes GL interpolate
|
|
648
|
+
// linearly in screen space, which produces a visible fold along
|
|
649
|
+
// the shared diagonal once the paper is significantly rotated.
|
|
650
|
+
gl_Position = vec4(clipX * wp, clipY * wp, clipZ * wp, wp);
|
|
651
|
+
v_uv = a_uv;
|
|
652
|
+
}
|
|
653
|
+
`;
|
|
654
|
+
const SWITCH_FS = `#version 300 es
|
|
655
|
+
precision highp float;
|
|
656
|
+
|
|
657
|
+
in vec2 v_uv;
|
|
658
|
+
|
|
659
|
+
uniform sampler2D u_tex;
|
|
660
|
+
|
|
661
|
+
out vec4 outColor;
|
|
662
|
+
|
|
663
|
+
void main() {
|
|
664
|
+
outColor = texture(u_tex, v_uv);
|
|
665
|
+
}
|
|
666
|
+
`;
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
// GL helpers
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
function createProgram(gl, vsSource, fsSource) {
|
|
671
|
+
const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
672
|
+
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
673
|
+
if (!vs || !fs) {
|
|
674
|
+
if (vs)
|
|
675
|
+
gl.deleteShader(vs);
|
|
676
|
+
if (fs)
|
|
677
|
+
gl.deleteShader(fs);
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const program = gl.createProgram();
|
|
681
|
+
if (!program) {
|
|
682
|
+
gl.deleteShader(vs);
|
|
683
|
+
gl.deleteShader(fs);
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
gl.attachShader(program, vs);
|
|
687
|
+
gl.attachShader(program, fs);
|
|
688
|
+
gl.linkProgram(program);
|
|
689
|
+
gl.deleteShader(vs);
|
|
690
|
+
gl.deleteShader(fs);
|
|
691
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
692
|
+
gl.deleteProgram(program);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
return program;
|
|
696
|
+
}
|
|
697
|
+
function compileShader(gl, type, source) {
|
|
698
|
+
const shader = gl.createShader(type);
|
|
699
|
+
if (!shader)
|
|
700
|
+
return null;
|
|
701
|
+
gl.shaderSource(shader, source);
|
|
702
|
+
gl.compileShader(shader);
|
|
703
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
704
|
+
gl.deleteShader(shader);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
return shader;
|
|
708
|
+
}
|
|
709
|
+
function bindAttrib(gl, program, name, size, stride, offset) {
|
|
710
|
+
const loc = gl.getAttribLocation(program, name);
|
|
711
|
+
if (loc < 0)
|
|
712
|
+
return;
|
|
713
|
+
gl.enableVertexAttribArray(loc);
|
|
714
|
+
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, stride, offset);
|
|
715
|
+
}
|
|
716
|
+
function uploadTexture(gl, source) {
|
|
717
|
+
const tex = gl.createTexture();
|
|
718
|
+
if (!tex)
|
|
719
|
+
return null;
|
|
720
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
721
|
+
// Keep UNPACK_FLIP_Y_WEBGL at its default (false): the source canvas's
|
|
722
|
+
// top-left pixel lands at texel (0, 0), so uv (0, 0) samples the image
|
|
723
|
+
// top-left — matching the screen-space UV convention used by the
|
|
724
|
+
// vertex shader (which itself flips Y when emitting clip coords).
|
|
725
|
+
// Using UNPACK_FLIP_Y=true here would cancel the shader's Y flip and
|
|
726
|
+
// render the slide upside-down.
|
|
727
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
728
|
+
try {
|
|
729
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
gl.deleteTexture(tex);
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
736
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
737
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
738
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
739
|
+
return tex;
|
|
740
|
+
}
|
|
741
|
+
function buildVortexMesh(cols, rows, slideW, slideH, dir) {
|
|
742
|
+
const tileW = slideW / cols;
|
|
743
|
+
const tileH = slideH / rows;
|
|
744
|
+
const FLOATS_PER_VERT = 11;
|
|
745
|
+
const VERTS_PER_TILE = 6;
|
|
746
|
+
const totalVerts = rows * cols * VERTS_PER_TILE;
|
|
747
|
+
const data = new Float32Array(totalVerts * FLOATS_PER_VERT);
|
|
748
|
+
let idx = 0;
|
|
749
|
+
for (let r = 0; r < rows; r++) {
|
|
750
|
+
for (let c = 0; c < cols; c++) {
|
|
751
|
+
const tileCenterX = (c + 0.5) * tileW;
|
|
752
|
+
const tileCenterY = (r + 0.5) * tileH;
|
|
753
|
+
const u0 = c / cols;
|
|
754
|
+
const u1 = (c + 1) / cols;
|
|
755
|
+
const v0 = r / rows;
|
|
756
|
+
const v1 = (r + 1) / rows;
|
|
757
|
+
// Per-tile stagger — mirrors setupVortex() in transition.ts so
|
|
758
|
+
// the GL path visually matches the CSS fallback.
|
|
759
|
+
const colN = c / Math.max(1, cols - 1);
|
|
760
|
+
const rowN = r / Math.max(1, rows - 1);
|
|
761
|
+
let outSt;
|
|
762
|
+
let inSt;
|
|
763
|
+
switch (dir) {
|
|
764
|
+
case "right":
|
|
765
|
+
outSt = colN * 0.7 + rowN * 0.3;
|
|
766
|
+
inSt = (1 - colN) * 0.7 + (1 - rowN) * 0.3;
|
|
767
|
+
break;
|
|
768
|
+
case "left":
|
|
769
|
+
outSt = (1 - colN) * 0.7 + rowN * 0.3;
|
|
770
|
+
inSt = colN * 0.7 + (1 - rowN) * 0.3;
|
|
771
|
+
break;
|
|
772
|
+
case "down":
|
|
773
|
+
outSt = rowN * 0.7 + colN * 0.3;
|
|
774
|
+
inSt = (1 - rowN) * 0.7 + (1 - colN) * 0.3;
|
|
775
|
+
break;
|
|
776
|
+
case "up":
|
|
777
|
+
outSt = (1 - rowN) * 0.7 + colN * 0.3;
|
|
778
|
+
inSt = rowN * 0.7 + (1 - colN) * 0.3;
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
const jitter = (Math.random() - 0.5) * 0.15;
|
|
782
|
+
const staggerOut = Math.max(0, Math.min(1, outSt + jitter));
|
|
783
|
+
const staggerIn = Math.max(0, Math.min(1, inSt + jitter));
|
|
784
|
+
const zSign = Math.random() < 0.5 ? -1 : 1;
|
|
785
|
+
const zRand = 0.5 + Math.random();
|
|
786
|
+
const driftRand = 0.3 + Math.random() * 1.4;
|
|
787
|
+
// Two triangles per tile: A,B,C and A,C,D
|
|
788
|
+
// A = top-left B = top-right
|
|
789
|
+
// D = bottom-left C = bottom-right
|
|
790
|
+
const hw = tileW / 2;
|
|
791
|
+
const hh = tileH / 2;
|
|
792
|
+
const tileVerts = [
|
|
793
|
+
[-hw, -hh, u0, v0], // A
|
|
794
|
+
[hw, -hh, u1, v0], // B
|
|
795
|
+
[hw, hh, u1, v1], // C
|
|
796
|
+
[-hw, -hh, u0, v0], // A
|
|
797
|
+
[hw, hh, u1, v1], // C
|
|
798
|
+
[-hw, hh, u0, v1], // D
|
|
799
|
+
];
|
|
800
|
+
for (const [lx, ly, u, v] of tileVerts) {
|
|
801
|
+
data[idx++] = lx;
|
|
802
|
+
data[idx++] = ly;
|
|
803
|
+
data[idx++] = tileCenterX;
|
|
804
|
+
data[idx++] = tileCenterY;
|
|
805
|
+
data[idx++] = u;
|
|
806
|
+
data[idx++] = v;
|
|
807
|
+
data[idx++] = staggerOut;
|
|
808
|
+
data[idx++] = staggerIn;
|
|
809
|
+
data[idx++] = zSign;
|
|
810
|
+
data[idx++] = zRand;
|
|
811
|
+
data[idx++] = driftRand;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return { data, vertexCount: totalVerts };
|
|
816
|
+
}
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// Easing — matches runTransition() in transition.ts.
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
function easeInOut(t) {
|
|
821
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
822
|
+
}
|
|
823
|
+
//# sourceMappingURL=transition-gl.js.map
|