@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.
Files changed (34) hide show
  1. package/README.md +4 -0
  2. package/dist/package.json +1 -1
  3. package/dist/src/UDocClient.d.ts +7 -0
  4. package/dist/src/UDocClient.d.ts.map +1 -1
  5. package/dist/src/UDocClient.js +5 -4
  6. package/dist/src/UDocClient.js.map +1 -1
  7. package/dist/src/UDocViewer.d.ts +1 -1
  8. package/dist/src/UDocViewer.d.ts.map +1 -1
  9. package/dist/src/UDocViewer.js +2 -2
  10. package/dist/src/UDocViewer.js.map +1 -1
  11. package/dist/src/ui/viewer/components/SubToolbar.d.ts.map +1 -1
  12. package/dist/src/ui/viewer/components/SubToolbar.js +9 -1
  13. package/dist/src/ui/viewer/components/SubToolbar.js.map +1 -1
  14. package/dist/src/ui/viewer/shell.d.ts +1 -1
  15. package/dist/src/ui/viewer/shell.d.ts.map +1 -1
  16. package/dist/src/ui/viewer/shell.js +4 -4
  17. package/dist/src/ui/viewer/shell.js.map +1 -1
  18. package/dist/src/ui/viewer/tools/AnnotationDrawController.d.ts.map +1 -1
  19. package/dist/src/ui/viewer/tools/AnnotationDrawController.js +13 -5
  20. package/dist/src/ui/viewer/tools/AnnotationDrawController.js.map +1 -1
  21. package/dist/src/ui/viewer/transition-gl.d.ts +25 -0
  22. package/dist/src/ui/viewer/transition-gl.d.ts.map +1 -0
  23. package/dist/src/ui/viewer/transition-gl.js +823 -0
  24. package/dist/src/ui/viewer/transition-gl.js.map +1 -0
  25. package/dist/src/ui/viewer/transition.d.ts.map +1 -1
  26. package/dist/src/ui/viewer/transition.js +9 -0
  27. package/dist/src/ui/viewer/transition.js.map +1 -1
  28. package/dist/src/wasm/udoc.d.ts +1 -1
  29. package/dist/src/wasm/udoc.js +3 -3
  30. package/dist/src/wasm/udoc_bg.wasm +0 -0
  31. package/dist/src/wasm/udoc_bg.wasm.d.ts +1 -1
  32. package/dist/src/worker/worker-inline.js +1 -1
  33. package/dist/src/worker/worker.js +3 -3
  34. 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