@docmentis/udoc-viewer 0.6.14 → 0.6.16
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 +36 -0
- package/dist/package.json +1 -1
- package/dist/src/UDocClient.d.ts +12 -0
- package/dist/src/UDocClient.d.ts.map +1 -1
- package/dist/src/UDocClient.js +25 -4
- package/dist/src/UDocClient.js.map +1 -1
- package/dist/src/UDocViewer.d.ts +14 -1
- package/dist/src/UDocViewer.d.ts.map +1 -1
- package/dist/src/UDocViewer.js +33 -1
- package/dist/src/UDocViewer.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/ui/viewer/components/FontsPanel.d.ts +12 -0
- package/dist/src/ui/viewer/components/FontsPanel.d.ts.map +1 -0
- package/dist/src/ui/viewer/components/FontsPanel.js +141 -0
- package/dist/src/ui/viewer/components/FontsPanel.js.map +1 -0
- package/dist/src/ui/viewer/components/LeftPanel.d.ts.map +1 -1
- package/dist/src/ui/viewer/components/LeftPanel.js +16 -1
- package/dist/src/ui/viewer/components/LeftPanel.js.map +1 -1
- package/dist/src/ui/viewer/components/Viewport.d.ts.map +1 -1
- package/dist/src/ui/viewer/components/Viewport.js +90 -40
- package/dist/src/ui/viewer/components/Viewport.js.map +1 -1
- package/dist/src/ui/viewer/i18n/ar.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/ar.js +4 -0
- package/dist/src/ui/viewer/i18n/ar.js.map +1 -1
- package/dist/src/ui/viewer/i18n/de.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/de.js +4 -0
- package/dist/src/ui/viewer/i18n/de.js.map +1 -1
- package/dist/src/ui/viewer/i18n/en.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/en.js +4 -0
- package/dist/src/ui/viewer/i18n/en.js.map +1 -1
- package/dist/src/ui/viewer/i18n/es.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/es.js +4 -0
- package/dist/src/ui/viewer/i18n/es.js.map +1 -1
- package/dist/src/ui/viewer/i18n/fr.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/fr.js +4 -0
- package/dist/src/ui/viewer/i18n/fr.js.map +1 -1
- package/dist/src/ui/viewer/i18n/ja.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/ja.js +4 -0
- package/dist/src/ui/viewer/i18n/ja.js.map +1 -1
- package/dist/src/ui/viewer/i18n/ko.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/ko.js +4 -0
- package/dist/src/ui/viewer/i18n/ko.js.map +1 -1
- package/dist/src/ui/viewer/i18n/pt-BR.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/pt-BR.js +4 -0
- package/dist/src/ui/viewer/i18n/pt-BR.js.map +1 -1
- package/dist/src/ui/viewer/i18n/ru.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/ru.js +4 -0
- package/dist/src/ui/viewer/i18n/ru.js.map +1 -1
- package/dist/src/ui/viewer/i18n/types.d.ts +3 -0
- package/dist/src/ui/viewer/i18n/types.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/zh-CN.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/zh-CN.js +4 -0
- package/dist/src/ui/viewer/i18n/zh-CN.js.map +1 -1
- package/dist/src/ui/viewer/i18n/zh-TW.d.ts.map +1 -1
- package/dist/src/ui/viewer/i18n/zh-TW.js +4 -0
- package/dist/src/ui/viewer/i18n/zh-TW.js.map +1 -1
- package/dist/src/ui/viewer/icons.d.ts +1 -0
- package/dist/src/ui/viewer/icons.d.ts.map +1 -1
- package/dist/src/ui/viewer/icons.js +1 -0
- package/dist/src/ui/viewer/icons.js.map +1 -1
- package/dist/src/ui/viewer/state.d.ts +2 -2
- package/dist/src/ui/viewer/state.d.ts.map +1 -1
- package/dist/src/ui/viewer/state.js +2 -1
- package/dist/src/ui/viewer/state.js.map +1 -1
- package/dist/src/ui/viewer/styles-inline.d.ts +1 -1
- package/dist/src/ui/viewer/styles-inline.d.ts.map +1 -1
- package/dist/src/ui/viewer/styles-inline.js +89 -0
- package/dist/src/ui/viewer/styles-inline.js.map +1 -1
- package/dist/src/ui/viewer/transition.js +443 -180
- package/dist/src/ui/viewer/transition.js.map +1 -1
- package/dist/src/wasm/udoc.d.ts +64 -18
- package/dist/src/wasm/udoc.js +82 -35
- package/dist/src/wasm/udoc_bg.wasm +0 -0
- package/dist/src/wasm/udoc_bg.wasm.d.ts +6 -4
- package/dist/src/worker/WorkerClient.d.ts +28 -4
- package/dist/src/worker/WorkerClient.d.ts.map +1 -1
- package/dist/src/worker/WorkerClient.js +91 -7
- package/dist/src/worker/WorkerClient.js.map +1 -1
- package/dist/src/worker/index.d.ts +1 -1
- package/dist/src/worker/index.d.ts.map +1 -1
- package/dist/src/worker/worker-inline.js +1 -1
- package/dist/src/worker/worker.d.ts +36 -3
- package/dist/src/worker/worker.d.ts.map +1 -1
- package/dist/src/worker/worker.js +102 -37
- package/dist/src/worker/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -150,23 +150,6 @@ function oppositeEightDir(dir) {
|
|
|
150
150
|
};
|
|
151
151
|
return map[dir];
|
|
152
152
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Build a clip-path: inset() that reveals the incoming element
|
|
155
|
-
* progressively from the given direction.
|
|
156
|
-
*/
|
|
157
|
-
function revealInset(dir, t) {
|
|
158
|
-
const p = (1 - t) * 100;
|
|
159
|
-
switch (dir) {
|
|
160
|
-
case "right":
|
|
161
|
-
return `inset(0 0 0 ${p}%)`;
|
|
162
|
-
case "left":
|
|
163
|
-
return `inset(0 ${p}% 0 0)`;
|
|
164
|
-
case "down":
|
|
165
|
-
return `inset(${p}% 0 0 0)`;
|
|
166
|
-
case "up":
|
|
167
|
-
return `inset(0 0 ${p}% 0)`;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
153
|
function eightDirToRevealInset(dir, t) {
|
|
171
154
|
const p = (1 - t) * 100;
|
|
172
155
|
let top = 0, right = 0, bottom = 0, left = 0;
|
|
@@ -190,14 +173,10 @@ function resolveEffect(effect, forward) {
|
|
|
190
173
|
return effect.throughBlack ? cutThroughBlack : null;
|
|
191
174
|
case "dissolve":
|
|
192
175
|
return dissolveEffect();
|
|
193
|
-
case "push":
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
case "wipe": {
|
|
198
|
-
const dir = forward ? effect.direction : oppositeSide(effect.direction);
|
|
199
|
-
return wipeEffect(dir);
|
|
200
|
-
}
|
|
176
|
+
case "push":
|
|
177
|
+
return pushEffect(effect.direction);
|
|
178
|
+
case "wipe":
|
|
179
|
+
return wipeEffect(effect.direction);
|
|
201
180
|
case "cover":
|
|
202
181
|
return coverEffect(oppositeEightDir(effect.direction));
|
|
203
182
|
case "uncover":
|
|
@@ -228,7 +207,7 @@ function resolveEffect(effect, forward) {
|
|
|
228
207
|
case "wheel":
|
|
229
208
|
return wheelSweepEffect(effect.spokes, true);
|
|
230
209
|
case "newsflash":
|
|
231
|
-
return
|
|
210
|
+
return newsflashEffect;
|
|
232
211
|
case "blinds":
|
|
233
212
|
return blindsEffect(effect.orientation);
|
|
234
213
|
case "checker":
|
|
@@ -355,10 +334,15 @@ function cutThroughBlack(t, outgoing, _incoming) {
|
|
|
355
334
|
* Push: snapshot slides away on top, incoming revealed via clip-path underneath.
|
|
356
335
|
*/
|
|
357
336
|
function pushEffect(dir) {
|
|
337
|
+
const opp = oppositeSide(dir);
|
|
358
338
|
return (t, outgoing, incoming) => {
|
|
339
|
+
if (t >= 1) {
|
|
340
|
+
incoming.style.transform = "";
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
359
343
|
outgoing.style.zIndex = "1";
|
|
360
344
|
outgoing.style.transform = sideToTranslate(dir, t);
|
|
361
|
-
incoming.style.
|
|
345
|
+
incoming.style.transform = sideToTranslate(opp, 1 - t);
|
|
362
346
|
};
|
|
363
347
|
}
|
|
364
348
|
/**
|
|
@@ -366,7 +350,28 @@ function pushEffect(dir) {
|
|
|
366
350
|
*/
|
|
367
351
|
function wipeEffect(dir) {
|
|
368
352
|
return (t, _outgoing, incoming) => {
|
|
369
|
-
|
|
353
|
+
if (t >= 1) {
|
|
354
|
+
clearMask(incoming);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const p = (1 - t) * 100;
|
|
358
|
+
const blur = Math.max(0.5, t * 50 * 0.08);
|
|
359
|
+
let rect;
|
|
360
|
+
switch (dir) {
|
|
361
|
+
case "right":
|
|
362
|
+
rect = `<rect x="${p}" y="0" width="${100 - p}" height="100" fill="white" filter="url(#b)"/>`;
|
|
363
|
+
break;
|
|
364
|
+
case "left":
|
|
365
|
+
rect = `<rect x="0" y="0" width="${100 - p}" height="100" fill="white" filter="url(#b)"/>`;
|
|
366
|
+
break;
|
|
367
|
+
case "down":
|
|
368
|
+
rect = `<rect x="0" y="0" width="100" height="${100 - p}" fill="white" filter="url(#b)"/>`;
|
|
369
|
+
break;
|
|
370
|
+
case "up":
|
|
371
|
+
rect = `<rect x="0" y="${p}" width="100" height="${100 - p}" fill="white" filter="url(#b)"/>`;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
applyMask(incoming, svgMask(rect, blur));
|
|
370
375
|
};
|
|
371
376
|
}
|
|
372
377
|
/**
|
|
@@ -390,10 +395,9 @@ function uncoverEffect(dir) {
|
|
|
390
395
|
* Pull: snapshot slides away on top while incoming is revealed via clip-path.
|
|
391
396
|
*/
|
|
392
397
|
function pullEffect(dir) {
|
|
393
|
-
return (t, outgoing,
|
|
398
|
+
return (t, outgoing, _incoming) => {
|
|
394
399
|
outgoing.style.zIndex = "1";
|
|
395
400
|
outgoing.style.transform = eightDirToTranslate(dir, t);
|
|
396
|
-
incoming.style.clipPath = eightDirToRevealInset(oppositeEightDir(dir), t);
|
|
397
401
|
};
|
|
398
402
|
}
|
|
399
403
|
/**
|
|
@@ -401,19 +405,36 @@ function pullEffect(dir) {
|
|
|
401
405
|
* Split "in": incoming revealed from edges inward — clip-path on snapshot (outgoing).
|
|
402
406
|
*/
|
|
403
407
|
function splitEffect(orientation, inOut) {
|
|
408
|
+
const isH = orientation === "horizontal";
|
|
404
409
|
if (inOut === "out") {
|
|
405
410
|
// Center outward: incoming starts fully clipped, opens from center
|
|
406
411
|
return (t, _outgoing, incoming) => {
|
|
412
|
+
if (t >= 1) {
|
|
413
|
+
clearMask(incoming);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
407
416
|
const p = (1 - t) * 50;
|
|
408
|
-
|
|
417
|
+
const blur = Math.max(0.5, t * 50 * 0.08);
|
|
418
|
+
const rect = isH
|
|
419
|
+
? `<rect x="0" y="${p}" width="100" height="${100 - 2 * p}" fill="white" filter="url(#b)"/>`
|
|
420
|
+
: `<rect x="${p}" y="0" width="${100 - 2 * p}" height="100" fill="white" filter="url(#b)"/>`;
|
|
421
|
+
applyMask(incoming, svgMask(rect, blur));
|
|
409
422
|
};
|
|
410
423
|
}
|
|
411
424
|
else {
|
|
412
425
|
// Edges inward: snapshot collapses toward center, revealing incoming at edges
|
|
413
426
|
return (t, outgoing, _incoming) => {
|
|
427
|
+
if (t >= 1) {
|
|
428
|
+
clearMask(outgoing);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
414
431
|
outgoing.style.zIndex = "1";
|
|
415
432
|
const p = t * 50;
|
|
416
|
-
|
|
433
|
+
const blur = Math.max(0.5, p * 0.08);
|
|
434
|
+
const rect = isH
|
|
435
|
+
? `<rect x="0" y="${p}" width="100" height="${100 - 2 * p}" fill="white" filter="url(#b)"/>`
|
|
436
|
+
: `<rect x="${p}" y="0" width="${100 - 2 * p}" height="100" fill="white" filter="url(#b)"/>`;
|
|
437
|
+
applyMask(outgoing, svgMask(rect, blur));
|
|
417
438
|
};
|
|
418
439
|
}
|
|
419
440
|
}
|
|
@@ -455,31 +476,86 @@ function circleEffect(t, _outgoing, incoming) {
|
|
|
455
476
|
return;
|
|
456
477
|
}
|
|
457
478
|
if (t <= 0) {
|
|
458
|
-
const mask = "radial-gradient(circle
|
|
479
|
+
const mask = "radial-gradient(circle at 50% 50%, #000 0%, transparent 0%)";
|
|
459
480
|
incoming.style.maskImage = mask;
|
|
460
481
|
incoming.style.setProperty("-webkit-mask-image", mask);
|
|
461
482
|
return;
|
|
462
483
|
}
|
|
463
|
-
|
|
464
|
-
|
|
484
|
+
// Color stops are relative to the gradient ray (farthest-corner by default),
|
|
485
|
+
// so r must reach 100% to fully cover the slide at t=1.
|
|
486
|
+
const r = t * 100;
|
|
487
|
+
const edge = Math.max(1, r * 0.15);
|
|
465
488
|
const inner = Math.max(0, r - edge);
|
|
466
|
-
const mask = `radial-gradient(circle
|
|
489
|
+
const mask = `radial-gradient(circle at 50% 50%, #000 ${inner}%, transparent ${r + 0.5}%)`;
|
|
467
490
|
incoming.style.maskImage = mask;
|
|
468
491
|
incoming.style.setProperty("-webkit-mask-image", mask);
|
|
469
492
|
}
|
|
470
|
-
/** Diamond reveal from center. */
|
|
493
|
+
/** Diamond reveal from center with feathered edge via SVG mask + blur. */
|
|
471
494
|
function diamondEffect(t, _outgoing, incoming) {
|
|
472
|
-
|
|
473
|
-
|
|
495
|
+
if (t >= 1) {
|
|
496
|
+
clearMask(incoming);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const p = t * 100;
|
|
500
|
+
const blur = Math.max(0.5, p * 0.08);
|
|
501
|
+
applyMask(incoming, diamondMaskSvg(p, blur));
|
|
502
|
+
}
|
|
503
|
+
/** Shared helper: wrap SVG shape content into a blurred mask data-URI. */
|
|
504
|
+
function svgMask(shapeContent, blur) {
|
|
505
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">` +
|
|
506
|
+
`<defs><filter id="b" x="-50%" y="-50%" width="200%" height="200%">` +
|
|
507
|
+
`<feGaussianBlur stdDeviation="${blur}"/></filter></defs>` +
|
|
508
|
+
shapeContent +
|
|
509
|
+
`</svg>`;
|
|
510
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
474
511
|
}
|
|
475
|
-
|
|
512
|
+
function applyMask(el, mask) {
|
|
513
|
+
el.style.maskImage = mask;
|
|
514
|
+
el.style.maskSize = "100% 100%";
|
|
515
|
+
el.style.setProperty("-webkit-mask-image", mask);
|
|
516
|
+
el.style.setProperty("-webkit-mask-size", "100% 100%");
|
|
517
|
+
}
|
|
518
|
+
function clearMask(el) {
|
|
519
|
+
el.style.maskImage = "";
|
|
520
|
+
el.style.setProperty("-webkit-mask-image", "");
|
|
521
|
+
}
|
|
522
|
+
function diamondMaskSvg(p, blur) {
|
|
523
|
+
return svgMask(`<polygon points="50,${50 - p} ${50 + p},50 50,${50 + p} ${50 - p},50" fill="white" filter="url(#b)"/>`, blur);
|
|
524
|
+
}
|
|
525
|
+
/** Plus/cross reveal from center with feathered edge via SVG mask + blur. */
|
|
476
526
|
function plusEffect(t, _outgoing, incoming) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
527
|
+
if (t >= 1) {
|
|
528
|
+
clearMask(incoming);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const p = t * 50;
|
|
532
|
+
const blur = Math.max(0.5, p * 0.08);
|
|
533
|
+
applyMask(incoming, plusMaskSvg(p, blur));
|
|
534
|
+
}
|
|
535
|
+
function plusMaskSvg(p, blur) {
|
|
536
|
+
// Plus shape: a cross centered at (50,50) with arm half-widths of p
|
|
537
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">` +
|
|
538
|
+
`<defs><filter id="b" x="-50%" y="-50%" width="200%" height="200%">` +
|
|
539
|
+
`<feGaussianBlur stdDeviation="${blur}"/></filter></defs>` +
|
|
540
|
+
`<polygon points="${50 - p},0 ${50 + p},0 ${50 + p},${50 - p} 100,${50 - p} ` +
|
|
541
|
+
`100,${50 + p} ${50 + p},${50 + p} ${50 + p},100 ${50 - p},100 ` +
|
|
542
|
+
`${50 - p},${50 + p} 0,${50 + p} 0,${50 - p} ${50 - p},${50 - p}" ` +
|
|
543
|
+
`fill="white" filter="url(#b)"/></svg>`;
|
|
544
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
545
|
+
}
|
|
546
|
+
/** Newsflash: incoming slide expands from a single pixel at center while rotating 360°. */
|
|
547
|
+
function newsflashEffect(t, _outgoing, incoming) {
|
|
548
|
+
if (t >= 1) {
|
|
549
|
+
incoming.style.transform = "";
|
|
550
|
+
incoming.style.opacity = "";
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// Scale from near-zero to 1, rotate a full 360°.
|
|
554
|
+
const scale = Math.max(0.01, t);
|
|
555
|
+
const deg = -t * 360;
|
|
556
|
+
incoming.style.transform = `scale(${scale}) rotate(${deg}deg)`;
|
|
557
|
+
incoming.style.transformOrigin = "50% 50%";
|
|
558
|
+
incoming.style.opacity = String(t);
|
|
483
559
|
}
|
|
484
560
|
/**
|
|
485
561
|
* Strips: diagonal wipe that reveals the incoming slide from a corner.
|
|
@@ -488,125 +564,235 @@ function plusEffect(t, _outgoing, incoming) {
|
|
|
488
564
|
function stripsEffect(dir) {
|
|
489
565
|
return (t, _outgoing, incoming) => {
|
|
490
566
|
if (t >= 1) {
|
|
491
|
-
incoming
|
|
567
|
+
clearMask(incoming);
|
|
492
568
|
return;
|
|
493
569
|
}
|
|
494
570
|
if (t <= 0) {
|
|
495
|
-
incoming
|
|
571
|
+
applyMask(incoming, svgMask(`<polygon points="0,0 0,0 0,0" fill="white" filter="url(#b)"/>`, 0));
|
|
496
572
|
return;
|
|
497
573
|
}
|
|
498
|
-
// d goes from 0 to 200 — the diagonal sweep distance across the rectangle
|
|
499
574
|
const d = t * 200;
|
|
575
|
+
const blur = Math.max(0.5, t * 50 * 0.08);
|
|
500
576
|
let points;
|
|
501
577
|
if (d <= 100) {
|
|
502
|
-
// Triangle phase: expanding triangle from the corner
|
|
503
578
|
switch (dir) {
|
|
504
579
|
case "rightDown":
|
|
505
|
-
points = `0
|
|
580
|
+
points = `0,0 ${d},0 0,${d}`;
|
|
506
581
|
break;
|
|
507
582
|
case "leftDown":
|
|
508
|
-
points = `100
|
|
583
|
+
points = `100,0 ${100 - d},0 100,${d}`;
|
|
509
584
|
break;
|
|
510
585
|
case "rightUp":
|
|
511
|
-
points = `0
|
|
586
|
+
points = `0,100 ${d},100 0,${100 - d}`;
|
|
512
587
|
break;
|
|
513
588
|
case "leftUp":
|
|
514
|
-
points = `100
|
|
589
|
+
points = `100,100 ${100 - d},100 100,${100 - d}`;
|
|
515
590
|
break;
|
|
516
591
|
}
|
|
517
592
|
}
|
|
518
593
|
else {
|
|
519
|
-
// Quadrilateral phase: the diagonal has passed the opposite edges
|
|
520
594
|
const e = d - 100;
|
|
521
595
|
switch (dir) {
|
|
522
596
|
case "rightDown":
|
|
523
|
-
points = `0
|
|
597
|
+
points = `0,0 100,0 100,${e} ${e},100 0,100`;
|
|
524
598
|
break;
|
|
525
599
|
case "leftDown":
|
|
526
|
-
points = `100
|
|
600
|
+
points = `100,0 0,0 0,${e} ${100 - e},100 100,100`;
|
|
527
601
|
break;
|
|
528
602
|
case "rightUp":
|
|
529
|
-
points = `0
|
|
603
|
+
points = `0,100 100,100 100,${100 - e} ${e},0 0,0`;
|
|
530
604
|
break;
|
|
531
605
|
case "leftUp":
|
|
532
|
-
points = `100
|
|
606
|
+
points = `100,100 0,100 0,${100 - e} ${100 - e},0 100,0`;
|
|
533
607
|
break;
|
|
534
608
|
}
|
|
535
609
|
}
|
|
536
|
-
incoming
|
|
610
|
+
applyMask(incoming, svgMask(`<polygon points="${points}" fill="white" filter="url(#b)"/>`, blur));
|
|
537
611
|
};
|
|
538
612
|
}
|
|
539
613
|
// ---------------------------------------------------------------------------
|
|
540
614
|
// Blinds / Comb
|
|
541
615
|
// ---------------------------------------------------------------------------
|
|
542
|
-
const BLIND_STRIPS =
|
|
616
|
+
const BLIND_STRIPS = 14;
|
|
543
617
|
/**
|
|
544
|
-
* Blinds: 3D
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
618
|
+
* Blinds: CSS 3D box-rotation effect matching PowerPoint.
|
|
619
|
+
*
|
|
620
|
+
* Each strip is modelled as a 3D box whose cross-section is a square
|
|
621
|
+
* (depth === strip height). The front face shows the outgoing slide and the
|
|
622
|
+
* bottom face shows the incoming slide. All boxes rotate 90° around the X
|
|
623
|
+
* axis (horizontal) or Y axis (vertical) at the box centre, so the bottom
|
|
624
|
+
* face rolls into view like a real window blind.
|
|
625
|
+
*
|
|
626
|
+
* All faces share a single `preserve-3d` context rooted on the outgoing
|
|
627
|
+
* snapshot element, so the browser z-sorts faces across strips correctly.
|
|
548
628
|
*/
|
|
549
629
|
function blindsEffect(orientation) {
|
|
550
630
|
const N = BLIND_STRIPS;
|
|
631
|
+
const isH = orientation === "horizontal";
|
|
632
|
+
let setup = null;
|
|
551
633
|
return (t, outgoing, incoming) => {
|
|
552
634
|
if (t >= 1) {
|
|
635
|
+
incoming.style.opacity = "";
|
|
553
636
|
incoming.style.clipPath = "none";
|
|
554
|
-
outgoing
|
|
637
|
+
// outgoing (now containing strips) is removed by onComplete
|
|
555
638
|
return;
|
|
556
639
|
}
|
|
557
|
-
|
|
640
|
+
// Lazy init: build 3D strip DOM on first frame
|
|
641
|
+
if (!setup) {
|
|
642
|
+
setup = setupBlinds3D(outgoing, incoming, N, isH);
|
|
643
|
+
}
|
|
644
|
+
// Fallback to crossfade if 3D setup failed (no canvases)
|
|
645
|
+
if (!setup || setup.flippers.length === 0) {
|
|
558
646
|
outgoing.style.zIndex = "1";
|
|
559
|
-
|
|
647
|
+
outgoing.style.opacity = `${1 - t}`;
|
|
560
648
|
return;
|
|
561
649
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
650
|
+
// Staggered rotation: center strips start first, edges start last.
|
|
651
|
+
// Each strip's local progress is derived from its distance to center.
|
|
652
|
+
const hd = setup.halfDepth;
|
|
653
|
+
const stagger = 0.7; // fraction of total duration used for stagger spread
|
|
654
|
+
const center = (N - 1) / 2;
|
|
655
|
+
for (let i = 0; i < setup.flippers.length; i++) {
|
|
656
|
+
const dist = Math.abs(i - center) / center; // 0 at center, 1 at edges
|
|
657
|
+
const delay = dist * stagger;
|
|
658
|
+
const stripT = Math.max(0, Math.min(1, (t - delay) / (1 - stagger)));
|
|
659
|
+
const angle = stripT * 90;
|
|
660
|
+
setup.flippers[i].style.transform = isH
|
|
661
|
+
? `translateZ(-${hd}px) rotateX(${angle}deg)`
|
|
662
|
+
: `translateZ(-${hd}px) rotateY(-${angle}deg)`;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Build N 3D box-strip elements inside the outgoing snapshot.
|
|
668
|
+
*
|
|
669
|
+
* DOM structure (horizontal example):
|
|
670
|
+
* outgoing [perspective, preserve-3d]
|
|
671
|
+
* ├── flipper-0 [preserve-3d, position: absolute, rotateX(angle)]
|
|
672
|
+
* │ ├── front [canvas, translateZ(d/2)]
|
|
673
|
+
* │ └── bottom [canvas, rotateX(-90deg) translateZ(d/2)]
|
|
674
|
+
* ├── flipper-1 ...
|
|
675
|
+
* └── ...
|
|
676
|
+
*/
|
|
677
|
+
function setupBlinds3D(outgoing, incoming, N, isH) {
|
|
678
|
+
// The outgoing snapshot is a div (positioned at the slot area) containing a canvas.
|
|
679
|
+
// The incoming is the real spread — find its main canvas.
|
|
680
|
+
const outCanvas = outgoing.querySelector("canvas");
|
|
681
|
+
const inCanvas = incoming.querySelector(".udoc-spread__canvas") ??
|
|
682
|
+
incoming.querySelector("canvas");
|
|
683
|
+
if (!outCanvas || !inCanvas || outCanvas.width === 0 || inCanvas.width === 0) {
|
|
684
|
+
return { flippers: [], halfDepth: 0 };
|
|
685
|
+
}
|
|
686
|
+
// Derive CSS dimensions from the canvas physical size / DPR so they are
|
|
687
|
+
// on exact device-pixel boundaries (the snapshot is built this way).
|
|
688
|
+
const dpr = window.devicePixelRatio || 1;
|
|
689
|
+
const slideW = outCanvas.width / dpr;
|
|
690
|
+
const slideH = outCanvas.height / dpr;
|
|
691
|
+
const d = isH ? slideH / N : slideW / N; // box depth = strip size (square cross-section)
|
|
692
|
+
// Repurpose the disposable snapshot as the container.
|
|
693
|
+
// Since the snapshot is now a canvas positioned at the slot area,
|
|
694
|
+
// all children are placed at (0,0) relative to it — no offset needed.
|
|
695
|
+
outgoing.innerHTML = "";
|
|
696
|
+
outgoing.style.overflow = "visible";
|
|
697
|
+
outgoing.style.zIndex = "1";
|
|
698
|
+
// Hide the real spread during animation (resetIncoming restores on cancel)
|
|
699
|
+
incoming.style.opacity = "0";
|
|
700
|
+
// Black backdrop — flat 2D element, NOT inside the preserve-3d context.
|
|
701
|
+
// Sits behind the 3D scene in normal stacking order (DOM before scene).
|
|
702
|
+
const backdrop = document.createElement("div");
|
|
703
|
+
backdrop.style.position = "absolute";
|
|
704
|
+
backdrop.style.left = "0";
|
|
705
|
+
backdrop.style.top = "0";
|
|
706
|
+
backdrop.style.width = `${slideW}px`;
|
|
707
|
+
backdrop.style.height = `${slideH}px`;
|
|
708
|
+
backdrop.style.background = "#000";
|
|
709
|
+
outgoing.appendChild(backdrop);
|
|
710
|
+
// 3D scene container — preserve-3d + perspective live here,
|
|
711
|
+
// separate from the flat backdrop so it doesn't interfere.
|
|
712
|
+
const scene = document.createElement("div");
|
|
713
|
+
scene.style.position = "absolute";
|
|
714
|
+
scene.style.left = "0";
|
|
715
|
+
scene.style.top = "0";
|
|
716
|
+
scene.style.width = `${slideW}px`;
|
|
717
|
+
scene.style.height = `${slideH}px`;
|
|
718
|
+
scene.style.perspective = `${(isH ? slideH : slideW) * 2}px`;
|
|
719
|
+
scene.style.transformStyle = "preserve-3d";
|
|
720
|
+
outgoing.appendChild(scene);
|
|
721
|
+
const flippers = [];
|
|
722
|
+
for (let i = 0; i < N; i++) {
|
|
723
|
+
// Flipper — the rotating 3D box for this strip
|
|
724
|
+
const flipper = document.createElement("div");
|
|
725
|
+
flipper.style.position = "absolute";
|
|
726
|
+
flipper.style.transformStyle = "preserve-3d";
|
|
727
|
+
const pad = 0.5; // half-pixel overlap to hide sub-pixel seams
|
|
728
|
+
if (isH) {
|
|
729
|
+
flipper.style.left = "0";
|
|
730
|
+
flipper.style.top = `${i * d - pad}px`;
|
|
731
|
+
flipper.style.width = `${slideW}px`;
|
|
732
|
+
flipper.style.height = `${d + pad * 2}px`;
|
|
585
733
|
}
|
|
586
734
|
else {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (orientation === "horizontal") {
|
|
592
|
-
const stripH = 100 / N;
|
|
593
|
-
for (let i = 0; i < N; i++) {
|
|
594
|
-
const top = i * stripH;
|
|
595
|
-
const bot = top + frac * stripH;
|
|
596
|
-
inPoints.push("0% 0%", `0% ${top}%`, `100% ${top}%`, `100% ${bot}%`, `0% ${bot}%`, `0% ${top}%`, "0% 0%");
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
else {
|
|
600
|
-
const stripW = 100 / N;
|
|
601
|
-
for (let i = 0; i < N; i++) {
|
|
602
|
-
const left = i * stripW;
|
|
603
|
-
const right = left + frac * stripW;
|
|
604
|
-
inPoints.push("0% 0%", `${left}% 0%`, `${right}% 0%`, `${right}% 100%`, `${left}% 100%`, `${left}% 0%`, "0% 0%");
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
incoming.style.clipPath = `polygon(${inPoints.join(", ")})`;
|
|
735
|
+
flipper.style.top = "0";
|
|
736
|
+
flipper.style.left = `${i * d - pad}px`;
|
|
737
|
+
flipper.style.width = `${d + pad * 2}px`;
|
|
738
|
+
flipper.style.height = `${slideH}px`;
|
|
608
739
|
}
|
|
609
|
-
|
|
740
|
+
// transform-origin defaults to center — correct for box-centre rotation
|
|
741
|
+
// Front face: outgoing content, pushed forward by d/2
|
|
742
|
+
const front = createBlindFace(outCanvas, i, N, isH, pad);
|
|
743
|
+
front.style.backfaceVisibility = "hidden";
|
|
744
|
+
front.style.transform = `translateZ(${d / 2}px)`;
|
|
745
|
+
// Bottom/right face: incoming content, rotated into position then pushed out
|
|
746
|
+
const bottom = createBlindFace(inCanvas, i, N, isH, pad);
|
|
747
|
+
bottom.style.backfaceVisibility = "hidden";
|
|
748
|
+
bottom.style.transform = isH
|
|
749
|
+
? `rotateX(-90deg) translateZ(${d / 2}px)`
|
|
750
|
+
: `rotateY(90deg) translateZ(${d / 2}px)`;
|
|
751
|
+
flipper.appendChild(front);
|
|
752
|
+
flipper.appendChild(bottom);
|
|
753
|
+
scene.appendChild(flipper);
|
|
754
|
+
flippers.push(flipper);
|
|
755
|
+
}
|
|
756
|
+
return { flippers, halfDepth: d / 2 };
|
|
757
|
+
}
|
|
758
|
+
/** Create a canvas element showing one strip slice of a source canvas.
|
|
759
|
+
* The slice includes `pad` CSS-pixel overlap on each side so that
|
|
760
|
+
* `width:100%; height:100%` fills the padded flipper without scaling. */
|
|
761
|
+
function createBlindFace(source, index, total, isHorizontal, pad) {
|
|
762
|
+
const dpr = window.devicePixelRatio || 1;
|
|
763
|
+
const padPx = Math.ceil(pad * dpr); // padding in device pixels
|
|
764
|
+
const canvas = document.createElement("canvas");
|
|
765
|
+
canvas.style.position = "absolute";
|
|
766
|
+
canvas.style.top = "0";
|
|
767
|
+
canvas.style.left = "0";
|
|
768
|
+
canvas.style.width = "100%";
|
|
769
|
+
canvas.style.height = "100%";
|
|
770
|
+
canvas.style.display = "block";
|
|
771
|
+
if (isHorizontal) {
|
|
772
|
+
const stripTop = Math.round((index * source.height) / total);
|
|
773
|
+
const stripBot = Math.round(((index + 1) * source.height) / total);
|
|
774
|
+
const sy = Math.max(0, stripTop - padPx);
|
|
775
|
+
const syEnd = Math.min(source.height, stripBot + padPx);
|
|
776
|
+
const sh = syEnd - sy;
|
|
777
|
+
canvas.width = source.width;
|
|
778
|
+
canvas.height = sh;
|
|
779
|
+
const ctx = canvas.getContext("2d");
|
|
780
|
+
if (ctx)
|
|
781
|
+
ctx.drawImage(source, 0, sy, source.width, sh, 0, 0, source.width, sh);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
const stripLeft = Math.round((index * source.width) / total);
|
|
785
|
+
const stripRight = Math.round(((index + 1) * source.width) / total);
|
|
786
|
+
const sx = Math.max(0, stripLeft - padPx);
|
|
787
|
+
const sxEnd = Math.min(source.width, stripRight + padPx);
|
|
788
|
+
const sw = sxEnd - sx;
|
|
789
|
+
canvas.width = sw;
|
|
790
|
+
canvas.height = source.height;
|
|
791
|
+
const ctx = canvas.getContext("2d");
|
|
792
|
+
if (ctx)
|
|
793
|
+
ctx.drawImage(source, sx, 0, sw, source.height, 0, 0, sw, source.height);
|
|
794
|
+
}
|
|
795
|
+
return canvas;
|
|
610
796
|
}
|
|
611
797
|
/**
|
|
612
798
|
* Comb: N strips revealed from alternating directions.
|
|
@@ -677,27 +863,28 @@ function wheelSweepEffect(spokes, clockwise) {
|
|
|
677
863
|
const dir = clockwise ? 1 : -1;
|
|
678
864
|
return (t, _outgoing, incoming) => {
|
|
679
865
|
if (t >= 1) {
|
|
680
|
-
incoming
|
|
866
|
+
clearMask(incoming);
|
|
681
867
|
return;
|
|
682
868
|
}
|
|
683
869
|
if (t <= 0) {
|
|
684
|
-
incoming
|
|
870
|
+
applyMask(incoming, svgMask(`<polygon points="50,50 50,50 50,50" fill="white" filter="url(#b)"/>`, 0));
|
|
685
871
|
return;
|
|
686
872
|
}
|
|
873
|
+
const blur = Math.max(0.3, t * 50 * 0.06);
|
|
687
874
|
const sectorAngle = 360 / n;
|
|
688
875
|
const sweep = t * sectorAngle;
|
|
689
|
-
|
|
876
|
+
let path = "";
|
|
690
877
|
for (let s = 0; s < n; s++) {
|
|
691
878
|
const startDeg = -90 + s * sectorAngle;
|
|
692
|
-
|
|
879
|
+
path += "M50,50 ";
|
|
693
880
|
for (let j = 0; j <= ARC_STEPS; j++) {
|
|
694
881
|
const deg = startDeg + dir * (j / ARC_STEPS) * sweep;
|
|
695
882
|
const rad = (deg * Math.PI) / 180;
|
|
696
|
-
|
|
883
|
+
path += `L${50 + SWEEP_RADIUS * Math.cos(rad)},${50 + SWEEP_RADIUS * Math.sin(rad)} `;
|
|
697
884
|
}
|
|
698
|
-
|
|
885
|
+
path += "Z ";
|
|
699
886
|
}
|
|
700
|
-
incoming
|
|
887
|
+
applyMask(incoming, svgMask(`<path d="${path}" fill="white" filter="url(#b)"/>`, blur));
|
|
701
888
|
};
|
|
702
889
|
}
|
|
703
890
|
/**
|
|
@@ -705,28 +892,30 @@ function wheelSweepEffect(spokes, clockwise) {
|
|
|
705
892
|
*/
|
|
706
893
|
function wedgeEffect(t, _outgoing, incoming) {
|
|
707
894
|
if (t >= 1) {
|
|
708
|
-
incoming
|
|
895
|
+
clearMask(incoming);
|
|
709
896
|
return;
|
|
710
897
|
}
|
|
711
898
|
if (t <= 0) {
|
|
712
|
-
incoming
|
|
899
|
+
applyMask(incoming, svgMask(`<polygon points="50,50 50,50 50,50" fill="white" filter="url(#b)"/>`, 0));
|
|
713
900
|
return;
|
|
714
901
|
}
|
|
902
|
+
const blur = Math.max(0.3, t * 50 * 0.06);
|
|
715
903
|
const sweep = t * 180;
|
|
716
|
-
|
|
904
|
+
let path = "M50,50 ";
|
|
717
905
|
// Counter-clockwise half
|
|
718
906
|
for (let j = ARC_STEPS; j >= 0; j--) {
|
|
719
907
|
const deg = -90 - (j / ARC_STEPS) * sweep;
|
|
720
908
|
const rad = (deg * Math.PI) / 180;
|
|
721
|
-
|
|
909
|
+
path += `L${50 + SWEEP_RADIUS * Math.cos(rad)},${50 + SWEEP_RADIUS * Math.sin(rad)} `;
|
|
722
910
|
}
|
|
723
911
|
// Clockwise half
|
|
724
912
|
for (let j = 0; j <= ARC_STEPS; j++) {
|
|
725
913
|
const deg = -90 + (j / ARC_STEPS) * sweep;
|
|
726
914
|
const rad = (deg * Math.PI) / 180;
|
|
727
|
-
|
|
915
|
+
path += `L${50 + SWEEP_RADIUS * Math.cos(rad)},${50 + SWEEP_RADIUS * Math.sin(rad)} `;
|
|
728
916
|
}
|
|
729
|
-
|
|
917
|
+
path += "Z";
|
|
918
|
+
applyMask(incoming, svgMask(`<path d="${path}" fill="white" filter="url(#b)"/>`, blur));
|
|
730
919
|
}
|
|
731
920
|
// ---------------------------------------------------------------------------
|
|
732
921
|
// Checker / Random Bar
|
|
@@ -734,79 +923,153 @@ function wedgeEffect(t, _outgoing, incoming) {
|
|
|
734
923
|
const CHECKER_COLS = 8;
|
|
735
924
|
const CHECKER_ROWS = 6;
|
|
736
925
|
/**
|
|
737
|
-
* Checker: 3D flip effect
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
926
|
+
* Checker: 3D tile-flip effect in a checkerboard pattern.
|
|
927
|
+
*
|
|
928
|
+
* The slide is divided into a grid of tiles. Each tile is a 3D object
|
|
929
|
+
* with the outgoing slide on the front face and the incoming slide on
|
|
930
|
+
* the back face. Tiles flip 180° around the X axis (across/horizontal)
|
|
931
|
+
* or Y axis (down/vertical), staggered in a checkerboard sweep pattern.
|
|
741
932
|
*/
|
|
742
933
|
function checkerEffect(orientation) {
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
934
|
+
const isH = orientation === "horizontal";
|
|
935
|
+
const cols = CHECKER_COLS;
|
|
936
|
+
const rows = CHECKER_ROWS;
|
|
937
|
+
let setup = null;
|
|
938
|
+
// Sweep across columns (horizontal) or down rows (vertical),
|
|
939
|
+
// with checkerboard phase offset.
|
|
940
|
+
const sweepLen = (isH ? cols : rows) + 1;
|
|
941
|
+
const flipDur = 3; // each tile takes 3 sweep-units to flip
|
|
942
|
+
const totalLen = sweepLen + flipDur;
|
|
748
943
|
return (t, outgoing, incoming) => {
|
|
749
944
|
if (t >= 1) {
|
|
945
|
+
incoming.style.opacity = "";
|
|
750
946
|
incoming.style.clipPath = "none";
|
|
751
|
-
outgoing.style.clipPath = "polygon(0% 0%, 0% 0%, 0% 0%)";
|
|
752
947
|
return;
|
|
753
948
|
}
|
|
754
|
-
|
|
755
|
-
|
|
949
|
+
if (!setup) {
|
|
950
|
+
setup = setupChecker3D(outgoing, incoming, cols, rows, isH);
|
|
951
|
+
}
|
|
756
952
|
const sweep = t * totalLen;
|
|
757
|
-
const outPoints = [];
|
|
758
|
-
const inPoints = [];
|
|
759
953
|
for (let r = 0; r < rows; r++) {
|
|
760
954
|
for (let c = 0; c < cols; c++) {
|
|
761
|
-
const
|
|
955
|
+
const idx = r * cols + c;
|
|
956
|
+
const pos = isH ? c : r;
|
|
762
957
|
const phase = (r + c) % 2;
|
|
763
958
|
const cellStart = pos + phase;
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
const top = r * cellH;
|
|
768
|
-
if (cellT <= 0) {
|
|
769
|
-
// Cell hasn't started flipping — outgoing fully visible
|
|
770
|
-
outPoints.push("0% 0%", `${left}% ${top}%`, `${left + cellW}% ${top}%`, `${left + cellW}% ${top + cellH}%`, `${left}% ${top + cellH}%`, `${left}% ${top}%`, "0% 0%");
|
|
771
|
-
}
|
|
772
|
-
else if (cellT < 0.5) {
|
|
773
|
-
// First half: outgoing cell narrows (flip away)
|
|
774
|
-
const frac = 1 - cellT * 2;
|
|
775
|
-
if (orientation === "horizontal") {
|
|
776
|
-
const bot = top + frac * cellH;
|
|
777
|
-
outPoints.push("0% 0%", `${left}% ${top}%`, `${left + cellW}% ${top}%`, `${left + cellW}% ${bot}%`, `${left}% ${bot}%`, `${left}% ${top}%`, "0% 0%");
|
|
778
|
-
}
|
|
779
|
-
else {
|
|
780
|
-
const right = left + frac * cellW;
|
|
781
|
-
outPoints.push("0% 0%", `${left}% ${top}%`, `${right}% ${top}%`, `${right}% ${top + cellH}%`, `${left}% ${top + cellH}%`, `${left}% ${top}%`, "0% 0%");
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
// Second half: incoming cell widens (flip toward viewer)
|
|
786
|
-
const frac = (cellT - 0.5) * 2;
|
|
787
|
-
if (orientation === "horizontal") {
|
|
788
|
-
const bot = top + frac * cellH;
|
|
789
|
-
inPoints.push("0% 0%", `${left}% ${top}%`, `${left + cellW}% ${top}%`, `${left + cellW}% ${bot}%`, `${left}% ${bot}%`, `${left}% ${top}%`, "0% 0%");
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
const right = left + frac * cellW;
|
|
793
|
-
inPoints.push("0% 0%", `${left}% ${top}%`, `${right}% ${top}%`, `${right}% ${top + cellH}%`, `${left}% ${top + cellH}%`, `${left}% ${top}%`, "0% 0%");
|
|
794
|
-
}
|
|
795
|
-
}
|
|
959
|
+
const cellT = Math.max(0, Math.min(1, (sweep - cellStart) / flipDur));
|
|
960
|
+
const angle = cellT * 180;
|
|
961
|
+
setup.flippers[idx].style.transform = isH ? `rotateY(-${angle}deg)` : `rotateX(${angle}deg)`;
|
|
796
962
|
}
|
|
797
963
|
}
|
|
798
|
-
outgoing.style.zIndex = "1";
|
|
799
|
-
outgoing.style.clipPath = outPoints.length
|
|
800
|
-
? `polygon(${outPoints.join(", ")})`
|
|
801
|
-
: "polygon(0% 0%, 0% 0%, 0% 0%)";
|
|
802
|
-
incoming.style.clipPath = inPoints.length ? `polygon(${inPoints.join(", ")})` : "polygon(0% 0%, 0% 0%, 0% 0%)";
|
|
803
964
|
};
|
|
804
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Build a grid of 3D flipper tiles for the checker transition.
|
|
968
|
+
*
|
|
969
|
+
* DOM structure:
|
|
970
|
+
* outgoing [container]
|
|
971
|
+
* ├── backdrop [black background]
|
|
972
|
+
* └── scene [perspective, preserve-3d]
|
|
973
|
+
* ├── flipper-0,0 [preserve-3d, rotateX/Y]
|
|
974
|
+
* │ ├── front [canvas tile of outgoing]
|
|
975
|
+
* │ └── back [canvas tile of incoming, rotated 180°]
|
|
976
|
+
* ├── flipper-0,1 ...
|
|
977
|
+
* └── ...
|
|
978
|
+
*/
|
|
979
|
+
function setupChecker3D(outgoing, incoming, cols, rows, isH) {
|
|
980
|
+
const outCanvas = outgoing.querySelector("canvas");
|
|
981
|
+
const inCanvas = incoming.querySelector(".udoc-spread__canvas") ??
|
|
982
|
+
incoming.querySelector("canvas");
|
|
983
|
+
if (!outCanvas || !inCanvas || outCanvas.width === 0 || inCanvas.width === 0) {
|
|
984
|
+
return { flippers: [] };
|
|
985
|
+
}
|
|
986
|
+
// Derive CSS dimensions from the canvas physical size / DPR so they are
|
|
987
|
+
// on exact device-pixel boundaries (the snapshot is built this way).
|
|
988
|
+
const dpr = window.devicePixelRatio || 1;
|
|
989
|
+
const slideW = outCanvas.width / dpr;
|
|
990
|
+
const slideH = outCanvas.height / dpr;
|
|
991
|
+
const tileW = slideW / cols;
|
|
992
|
+
const tileH = slideH / rows;
|
|
993
|
+
outgoing.innerHTML = "";
|
|
994
|
+
outgoing.style.overflow = "visible";
|
|
995
|
+
outgoing.style.zIndex = "1";
|
|
996
|
+
incoming.style.opacity = "0";
|
|
997
|
+
const backdrop = document.createElement("div");
|
|
998
|
+
backdrop.style.position = "absolute";
|
|
999
|
+
backdrop.style.left = "0";
|
|
1000
|
+
backdrop.style.top = "0";
|
|
1001
|
+
backdrop.style.width = `${slideW}px`;
|
|
1002
|
+
backdrop.style.height = `${slideH}px`;
|
|
1003
|
+
backdrop.style.background = "#000";
|
|
1004
|
+
outgoing.appendChild(backdrop);
|
|
1005
|
+
const scene = document.createElement("div");
|
|
1006
|
+
scene.style.position = "absolute";
|
|
1007
|
+
scene.style.left = "0";
|
|
1008
|
+
scene.style.top = "0";
|
|
1009
|
+
scene.style.width = `${slideW}px`;
|
|
1010
|
+
scene.style.height = `${slideH}px`;
|
|
1011
|
+
scene.style.perspective = `${Math.max(slideW, slideH) * 2}px`;
|
|
1012
|
+
scene.style.transformStyle = "preserve-3d";
|
|
1013
|
+
outgoing.appendChild(scene);
|
|
1014
|
+
const flippers = [];
|
|
1015
|
+
for (let r = 0; r < rows; r++) {
|
|
1016
|
+
for (let c = 0; c < cols; c++) {
|
|
1017
|
+
const flipper = document.createElement("div");
|
|
1018
|
+
flipper.style.position = "absolute";
|
|
1019
|
+
flipper.style.transformStyle = "preserve-3d";
|
|
1020
|
+
const pad = 0.5; // half-pixel overlap to hide sub-pixel seams
|
|
1021
|
+
flipper.style.left = `${c * tileW - pad}px`;
|
|
1022
|
+
flipper.style.top = `${r * tileH - pad}px`;
|
|
1023
|
+
flipper.style.width = `${tileW + pad * 2}px`;
|
|
1024
|
+
flipper.style.height = `${tileH + pad * 2}px`;
|
|
1025
|
+
const front = createCheckerFace(outCanvas, c, r, cols, rows, pad);
|
|
1026
|
+
front.style.backfaceVisibility = "hidden";
|
|
1027
|
+
const back = createCheckerFace(inCanvas, c, r, cols, rows, pad);
|
|
1028
|
+
back.style.backfaceVisibility = "hidden";
|
|
1029
|
+
back.style.transform = isH ? "rotateY(-180deg)" : "rotateX(180deg)";
|
|
1030
|
+
flipper.appendChild(front);
|
|
1031
|
+
flipper.appendChild(back);
|
|
1032
|
+
scene.appendChild(flipper);
|
|
1033
|
+
flippers.push(flipper);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return { flippers };
|
|
1037
|
+
}
|
|
1038
|
+
/** Create a canvas element showing one tile of a source canvas.
|
|
1039
|
+
* The slice includes `pad` CSS-pixel overlap on each side so that
|
|
1040
|
+
* `width:100%; height:100%` fills the padded flipper without scaling. */
|
|
1041
|
+
function createCheckerFace(source, col, row, cols, rows, pad) {
|
|
1042
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1043
|
+
const padPx = Math.ceil(pad * dpr);
|
|
1044
|
+
const canvas = document.createElement("canvas");
|
|
1045
|
+
canvas.style.position = "absolute";
|
|
1046
|
+
canvas.style.top = "0";
|
|
1047
|
+
canvas.style.left = "0";
|
|
1048
|
+
canvas.style.width = "100%";
|
|
1049
|
+
canvas.style.height = "100%";
|
|
1050
|
+
canvas.style.display = "block";
|
|
1051
|
+
const tileLeft = Math.round((col * source.width) / cols);
|
|
1052
|
+
const tileRight = Math.round(((col + 1) * source.width) / cols);
|
|
1053
|
+
const tileTop = Math.round((row * source.height) / rows);
|
|
1054
|
+
const tileBot = Math.round(((row + 1) * source.height) / rows);
|
|
1055
|
+
const sx = Math.max(0, tileLeft - padPx);
|
|
1056
|
+
const sxEnd = Math.min(source.width, tileRight + padPx);
|
|
1057
|
+
const sy = Math.max(0, tileTop - padPx);
|
|
1058
|
+
const syEnd = Math.min(source.height, tileBot + padPx);
|
|
1059
|
+
const sw = sxEnd - sx;
|
|
1060
|
+
const sh = syEnd - sy;
|
|
1061
|
+
canvas.width = sw;
|
|
1062
|
+
canvas.height = sh;
|
|
1063
|
+
const ctx = canvas.getContext("2d");
|
|
1064
|
+
if (ctx)
|
|
1065
|
+
ctx.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
1066
|
+
return canvas;
|
|
1067
|
+
}
|
|
805
1068
|
// ---------------------------------------------------------------------------
|
|
806
1069
|
// Dissolve
|
|
807
1070
|
// ---------------------------------------------------------------------------
|
|
808
|
-
const DISSOLVE_COLS =
|
|
809
|
-
const DISSOLVE_ROWS =
|
|
1071
|
+
const DISSOLVE_COLS = 60;
|
|
1072
|
+
const DISSOLVE_ROWS = 40;
|
|
810
1073
|
/**
|
|
811
1074
|
* Dissolve: random grid cells appear progressively, approximating PowerPoint's
|
|
812
1075
|
* pixelated dissolve pattern. Cells snap fully visible in shuffled order.
|