pdf-flipbook 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pdf-flipbook.esm.js +203 -243
- package/dist/pdf-flipbook.esm.js.map +3 -3
- package/dist/pdf-flipbook.esm.min.js +30 -14
- package/dist/pdf-flipbook.esm.min.js.map +3 -3
- package/dist/pdf-flipbook.umd.js +203 -243
- package/dist/pdf-flipbook.umd.js.map +3 -3
- package/package.json +3 -3
package/dist/pdf-flipbook.esm.js
CHANGED
|
@@ -21209,331 +21209,291 @@ if (typeof window !== "undefined") {
|
|
|
21209
21209
|
__webpack_exports__GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${__webpack_exports__version}/pdf.worker.min.mjs`;
|
|
21210
21210
|
}
|
|
21211
21211
|
var DEFAULTS = {
|
|
21212
|
-
|
|
21213
|
-
|
|
21212
|
+
/** Flip animation duration in ms */
|
|
21213
|
+
flipDuration: 700,
|
|
21214
|
+
/** Shadow opacity at peak of flip (0-1) */
|
|
21215
|
+
shadowIntensity: 0.4,
|
|
21216
|
+
/** Device-pixel-ratio cap for canvas rendering */
|
|
21214
21217
|
maxDpr: 2,
|
|
21218
|
+
/** Whether to show the toolbar */
|
|
21215
21219
|
toolbar: true,
|
|
21220
|
+
/** Toolbar theme: "light" | "dark" */
|
|
21216
21221
|
theme: "dark",
|
|
21222
|
+
/** Preload N pages ahead */
|
|
21223
|
+
preloadAhead: 2,
|
|
21224
|
+
/** Called when a page turn starts: (fromPage, toPage) */
|
|
21217
21225
|
onFlip: null,
|
|
21226
|
+
/** Called when all pages are loaded */
|
|
21218
21227
|
onReady: null,
|
|
21228
|
+
/** Called on load error */
|
|
21219
21229
|
onError: null
|
|
21220
21230
|
};
|
|
21221
21231
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
21222
|
-
var
|
|
21223
|
-
var
|
|
21232
|
+
var even = (n) => n % 2 === 0 ? n : n - 1;
|
|
21233
|
+
var isTouchDevice = () => "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
21224
21234
|
var PdfFlipbook = class {
|
|
21235
|
+
/**
|
|
21236
|
+
* @param {string|HTMLElement} container CSS selector or DOM element
|
|
21237
|
+
* @param {string} pdfUrl Remote or local PDF URL
|
|
21238
|
+
* @param {object} [options] See DEFAULTS
|
|
21239
|
+
*/
|
|
21225
21240
|
constructor(container, pdfUrl, options = {}) {
|
|
21226
21241
|
if (!pdfUrl) throw new Error("[PdfFlipbook] pdfUrl is required");
|
|
21227
21242
|
this._el = typeof container === "string" ? document.querySelector(container) : container;
|
|
21228
|
-
if (!this._el) throw new Error(
|
|
21243
|
+
if (!this._el) throw new Error(`[PdfFlipbook] container not found: ${container}`);
|
|
21229
21244
|
this._url = pdfUrl;
|
|
21230
21245
|
this._opts = { ...DEFAULTS, ...options };
|
|
21231
21246
|
this._pages = [];
|
|
21232
|
-
this.
|
|
21247
|
+
this._pdf = null;
|
|
21233
21248
|
this._total = 0;
|
|
21249
|
+
this._spread = 0;
|
|
21234
21250
|
this._flipping = false;
|
|
21235
|
-
this._animId = null;
|
|
21236
|
-
this._pageAspect = 1.414;
|
|
21237
21251
|
this._init();
|
|
21238
21252
|
}
|
|
21253
|
+
// ── Static workerSrc override ──────────────────────────────────────────────
|
|
21239
21254
|
static set workerSrc(src) {
|
|
21240
21255
|
__webpack_exports__GlobalWorkerOptions.workerSrc = src;
|
|
21241
21256
|
}
|
|
21257
|
+
// ═══ PUBLIC API ═══════════════════════════════════════════════════════════
|
|
21258
|
+
/** Go to the next spread */
|
|
21242
21259
|
next() {
|
|
21243
|
-
this.
|
|
21260
|
+
this._flip(1);
|
|
21244
21261
|
}
|
|
21262
|
+
/** Go to the previous spread */
|
|
21245
21263
|
prev() {
|
|
21246
|
-
this.
|
|
21264
|
+
this._flip(-1);
|
|
21247
21265
|
}
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
const target =
|
|
21251
|
-
|
|
21266
|
+
/** Jump directly to a page (1-indexed) */
|
|
21267
|
+
goTo(pageNum) {
|
|
21268
|
+
const target = clamp(pageNum - 1, 0, this._total - 1);
|
|
21269
|
+
const targetSpread = even(target);
|
|
21270
|
+
if (targetSpread === this._spread) return;
|
|
21271
|
+
const dir = targetSpread > this._spread ? 1 : -1;
|
|
21272
|
+
this._animateFlip(dir, targetSpread);
|
|
21252
21273
|
}
|
|
21274
|
+
/** Destroy the flipbook and clean up */
|
|
21253
21275
|
destroy() {
|
|
21254
|
-
|
|
21276
|
+
this._el.innerHTML = "";
|
|
21277
|
+
this._el.classList.remove("pfb", `pfb--${this._opts.theme}`);
|
|
21278
|
+
this._pages = [];
|
|
21255
21279
|
window.removeEventListener("resize", this._onResize);
|
|
21256
21280
|
window.removeEventListener("keydown", this._onKey);
|
|
21257
|
-
this._el.innerHTML = "";
|
|
21258
21281
|
}
|
|
21282
|
+
// ═══ PRIVATE ══════════════════════════════════════════════════════════════
|
|
21259
21283
|
async _init() {
|
|
21260
21284
|
this._buildShell();
|
|
21261
21285
|
try {
|
|
21262
21286
|
await this._loadPdf();
|
|
21263
21287
|
this._bindEvents();
|
|
21264
21288
|
this._opts.onReady?.();
|
|
21265
|
-
} catch (
|
|
21266
|
-
console.error("[PdfFlipbook]",
|
|
21267
|
-
this._showError(
|
|
21268
|
-
this._opts.onError?.(
|
|
21289
|
+
} catch (err) {
|
|
21290
|
+
console.error("[PdfFlipbook]", err);
|
|
21291
|
+
this._showError(err.message);
|
|
21292
|
+
this._opts.onError?.(err);
|
|
21269
21293
|
}
|
|
21270
21294
|
}
|
|
21295
|
+
// ── DOM Shell ──────────────────────────────────────────────────────────────
|
|
21271
21296
|
_buildShell() {
|
|
21272
21297
|
this._el.classList.add("pfb", `pfb--${this._opts.theme}`);
|
|
21273
21298
|
this._el.innerHTML = `
|
|
21274
21299
|
<div class="pfb__stage">
|
|
21275
|
-
<
|
|
21276
|
-
|
|
21277
|
-
|
|
21278
|
-
|
|
21300
|
+
<div class="pfb__book">
|
|
21301
|
+
<div class="pfb__page pfb__page--left">
|
|
21302
|
+
<div class="pfb__page-front"></div>
|
|
21303
|
+
<div class="pfb__page-back"></div>
|
|
21304
|
+
<div class="pfb__shadow pfb__shadow--left"></div>
|
|
21305
|
+
</div>
|
|
21306
|
+
<div class="pfb__spine"></div>
|
|
21307
|
+
<div class="pfb__page pfb__page--right">
|
|
21308
|
+
<div class="pfb__page-front"></div>
|
|
21309
|
+
<div class="pfb__page-back"></div>
|
|
21310
|
+
<div class="pfb__shadow pfb__shadow--right"></div>
|
|
21311
|
+
</div>
|
|
21312
|
+
</div>
|
|
21313
|
+
<button class="pfb__btn pfb__btn--prev" aria-label="Previous page">←</button>
|
|
21314
|
+
<button class="pfb__btn pfb__btn--next" aria-label="Next page">→</button>
|
|
21315
|
+
<div class="pfb__loader">
|
|
21316
|
+
<div class="pfb__spinner"></div>
|
|
21317
|
+
<span class="pfb__loader-text">Loading PDF\u2026</span>
|
|
21318
|
+
</div>
|
|
21279
21319
|
<div class="pfb__error" hidden></div>
|
|
21280
21320
|
</div>
|
|
21281
|
-
${this._opts.toolbar ?
|
|
21282
|
-
|
|
21321
|
+
${this._opts.toolbar ? `
|
|
21322
|
+
<div class="pfb__toolbar">
|
|
21323
|
+
<button class="pfb__tb-btn pfb__tb-prev" aria-label="Previous">←</button>
|
|
21283
21324
|
<span class="pfb__page-info"></span>
|
|
21284
|
-
<button class="pfb__tb-btn pfb__tb-next">→</button>
|
|
21325
|
+
<button class="pfb__tb-btn pfb__tb-next" aria-label="Next">→</button>
|
|
21285
21326
|
</div>` : ""}
|
|
21286
21327
|
`;
|
|
21287
21328
|
this._stage = this._el.querySelector(".pfb__stage");
|
|
21288
|
-
this.
|
|
21289
|
-
this._ctx = this._canvas.getContext("2d");
|
|
21329
|
+
this._book = this._el.querySelector(".pfb__book");
|
|
21290
21330
|
this._loader = this._el.querySelector(".pfb__loader");
|
|
21291
|
-
this._loaderTx = this._el.querySelector(".pfb__loader-text");
|
|
21292
21331
|
this._errorEl = this._el.querySelector(".pfb__error");
|
|
21293
21332
|
this._pageInfo = this._el.querySelector(".pfb__page-info");
|
|
21294
|
-
this.
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
this.
|
|
21300
|
-
|
|
21301
|
-
|
|
21302
|
-
this._canvas.style.height = H + "px";
|
|
21303
|
-
if (this._total) this._drawSpread();
|
|
21304
|
-
}
|
|
21333
|
+
this._leftFront = this._el.querySelector(".pfb__page--left .pfb__page-front");
|
|
21334
|
+
this._leftBack = this._el.querySelector(".pfb__page--left .pfb__page-back");
|
|
21335
|
+
this._leftShadow = this._el.querySelector(".pfb__shadow--left");
|
|
21336
|
+
this._rightFront = this._el.querySelector(".pfb__page--right .pfb__page-front");
|
|
21337
|
+
this._rightBack = this._el.querySelector(".pfb__page--right .pfb__page-back");
|
|
21338
|
+
this._rightShadow = this._el.querySelector(".pfb__shadow--right");
|
|
21339
|
+
}
|
|
21340
|
+
// ── PDF Loading ────────────────────────────────────────────────────────────
|
|
21305
21341
|
async _loadPdf() {
|
|
21306
|
-
const
|
|
21307
|
-
this.
|
|
21308
|
-
|
|
21309
|
-
|
|
21310
|
-
this._pageAspect = vp0.height / vp0.width;
|
|
21311
|
-
for (let i = 1; i <= this._total; i++) {
|
|
21312
|
-
if (this._loaderTx) this._loaderTx.textContent = `Loading ${i} / ${this._total}\u2026`;
|
|
21313
|
-
const page = await pdf.getPage(i);
|
|
21314
|
-
const dpr = clamp(window.devicePixelRatio || 1, 1, this._opts.maxDpr);
|
|
21315
|
-
const rW = 1200 * dpr;
|
|
21316
|
-
const vp = page.getViewport({ scale: rW / vp0.width });
|
|
21317
|
-
const cvs = document.createElement("canvas");
|
|
21318
|
-
cvs.width = vp.width;
|
|
21319
|
-
cvs.height = vp.height;
|
|
21320
|
-
await page.render({ canvasContext: cvs.getContext("2d"), viewport: vp }).promise;
|
|
21321
|
-
this._pages.push(cvs);
|
|
21322
|
-
}
|
|
21342
|
+
const loadingTask = __webpack_exports__getDocument({ url: this._url, withCredentials: false });
|
|
21343
|
+
this._pdf = await loadingTask.promise;
|
|
21344
|
+
this._total = this._pdf.numPages;
|
|
21345
|
+
await this._prerenderAll();
|
|
21323
21346
|
this._loader.hidden = true;
|
|
21324
|
-
this.
|
|
21347
|
+
this._renderSpread(0, false);
|
|
21348
|
+
}
|
|
21349
|
+
async _prerenderAll() {
|
|
21350
|
+
this._loaderText = this._el.querySelector(".pfb__loader-text");
|
|
21351
|
+
for (let i = 1; i <= this._total; i++) {
|
|
21352
|
+
const canvas = await this._renderPage(i);
|
|
21353
|
+
this._pages.push(canvas);
|
|
21354
|
+
if (this._loaderText) {
|
|
21355
|
+
this._loaderText.textContent = `Loading ${i} / ${this._total}\u2026`;
|
|
21356
|
+
}
|
|
21357
|
+
}
|
|
21358
|
+
}
|
|
21359
|
+
async _renderPage(pageNum) {
|
|
21360
|
+
const page = await this._pdf.getPage(pageNum);
|
|
21361
|
+
const dpr = clamp(window.devicePixelRatio || 1, 1, this._opts.maxDpr);
|
|
21362
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
21363
|
+
const baseW = 900;
|
|
21364
|
+
const scale = baseW / viewport.width;
|
|
21365
|
+
const vp = page.getViewport({ scale: scale * dpr });
|
|
21366
|
+
const canvas = document.createElement("canvas");
|
|
21367
|
+
canvas.width = vp.width;
|
|
21368
|
+
canvas.height = vp.height;
|
|
21369
|
+
canvas.style.width = `${vp.width / dpr}px`;
|
|
21370
|
+
canvas.style.height = `${vp.height / dpr}px`;
|
|
21371
|
+
canvas.dataset.pageNum = pageNum;
|
|
21372
|
+
const ctx = canvas.getContext("2d");
|
|
21373
|
+
await page.render({ canvasContext: ctx, viewport: vp }).promise;
|
|
21374
|
+
return canvas;
|
|
21375
|
+
}
|
|
21376
|
+
// ── Spread Rendering ───────────────────────────────────────────────────────
|
|
21377
|
+
_renderSpread(spreadIndex, animate) {
|
|
21378
|
+
this._spread = clamp(spreadIndex, 0, even(this._total - 1) || 0);
|
|
21379
|
+
const leftIdx = this._spread;
|
|
21380
|
+
const rightIdx = this._spread + 1;
|
|
21381
|
+
this._setFace(this._leftFront, leftIdx);
|
|
21382
|
+
this._setFace(this._rightFront, rightIdx);
|
|
21383
|
+
this._setFace(this._leftBack, null);
|
|
21384
|
+
this._setFace(this._rightBack, null);
|
|
21325
21385
|
this._updateUI();
|
|
21326
21386
|
}
|
|
21327
|
-
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
21338
|
-
|
|
21339
|
-
bH = bW / 2 * this._pageAspect;
|
|
21340
|
-
}
|
|
21341
|
-
}
|
|
21342
|
-
const x = (cW - bW) / 2, y = (cH - bH) / 2;
|
|
21343
|
-
return { x, y, w: bW, h: bH, mobile, pageW: mobile ? bW : bW / 2, pageH: bH };
|
|
21344
|
-
}
|
|
21345
|
-
_drawPage(ctx, canvas, px, py, pw, ph) {
|
|
21346
|
-
ctx.fillStyle = "#fdfaf5";
|
|
21347
|
-
ctx.fillRect(px, py, pw, ph);
|
|
21348
|
-
if (canvas) ctx.drawImage(canvas, px, py, pw, ph);
|
|
21349
|
-
ctx.strokeStyle = "rgba(0,0,0,0.07)";
|
|
21350
|
-
ctx.lineWidth = 1;
|
|
21351
|
-
ctx.strokeRect(px + 0.5, py + 0.5, pw - 1, ph - 1);
|
|
21352
|
-
}
|
|
21353
|
-
_drawSpine(ctx, sx, sy, sh) {
|
|
21354
|
-
const g = ctx.createLinearGradient(sx - 10, 0, sx + 10, 0);
|
|
21355
|
-
g.addColorStop(0, "rgba(0,0,0,0.2)");
|
|
21356
|
-
g.addColorStop(0.45, "rgba(0,0,0,0.5)");
|
|
21357
|
-
g.addColorStop(0.55, "rgba(0,0,0,0.5)");
|
|
21358
|
-
g.addColorStop(1, "rgba(0,0,0,0.08)");
|
|
21359
|
-
ctx.fillStyle = g;
|
|
21360
|
-
ctx.fillRect(sx - 10, sy, 20, sh);
|
|
21361
|
-
}
|
|
21362
|
-
_drawBookShadow(bx, by, bw, bh) {
|
|
21363
|
-
const ctx = this._ctx;
|
|
21364
|
-
const g = ctx.createLinearGradient(0, by + bh, 0, by + bh + 40);
|
|
21365
|
-
g.addColorStop(0, "rgba(0,0,0,0.35)");
|
|
21366
|
-
g.addColorStop(1, "rgba(0,0,0,0)");
|
|
21367
|
-
ctx.fillStyle = g;
|
|
21368
|
-
ctx.fillRect(bx, by + bh - 4, bw, 44);
|
|
21369
|
-
}
|
|
21370
|
-
_drawSpread(flip) {
|
|
21371
|
-
const ctx = this._ctx;
|
|
21372
|
-
const { x, y, w, h, mobile, pageW, pageH } = this._bookRect();
|
|
21373
|
-
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
|
21374
|
-
this._drawBookShadow(x, y, w, h);
|
|
21375
|
-
if (mobile) {
|
|
21376
|
-
this._drawPage(ctx, this._pages[this._spread] || null, x, y, pageW, pageH);
|
|
21377
|
-
} else {
|
|
21378
|
-
this._drawPage(ctx, this._pages[this._spread] || null, x, y, pageW, pageH);
|
|
21379
|
-
this._drawPage(ctx, this._pages[this._spread + 1] || null, x + pageW, y, pageW, pageH);
|
|
21380
|
-
this._drawSpine(ctx, x + pageW, y, pageH);
|
|
21387
|
+
_setFace(face, pageIndex) {
|
|
21388
|
+
face.innerHTML = "";
|
|
21389
|
+
if (pageIndex === null || pageIndex >= this._total) {
|
|
21390
|
+
face.classList.add("pfb__face--blank");
|
|
21391
|
+
return;
|
|
21392
|
+
}
|
|
21393
|
+
face.classList.remove("pfb__face--blank");
|
|
21394
|
+
const canvas = this._pages[pageIndex];
|
|
21395
|
+
if (canvas) {
|
|
21396
|
+
const clone = canvas.cloneNode();
|
|
21397
|
+
clone.getContext("2d").drawImage(canvas, 0, 0);
|
|
21398
|
+
face.appendChild(clone);
|
|
21381
21399
|
}
|
|
21382
|
-
if (flip) this._renderFlip(ctx, flip, x, y, pageW, pageH, mobile);
|
|
21383
21400
|
}
|
|
21384
|
-
|
|
21385
|
-
|
|
21386
|
-
|
|
21401
|
+
// ── Flip Animation ────────────────────────────────────────────────────────
|
|
21402
|
+
_flip(dir) {
|
|
21403
|
+
if (this._flipping) return;
|
|
21404
|
+
const next = this._spread + dir * 2;
|
|
21405
|
+
if (next < 0 || next > even(this._total - 1)) return;
|
|
21406
|
+
this._animateFlip(dir, next);
|
|
21407
|
+
}
|
|
21408
|
+
_animateFlip(dir, targetSpread) {
|
|
21409
|
+
if (this._flipping) return;
|
|
21410
|
+
this._flipping = true;
|
|
21411
|
+
const { flipDuration, shadowIntensity, onFlip } = this._opts;
|
|
21412
|
+
const currentLeft = this._spread;
|
|
21413
|
+
const currentRight = this._spread + 1;
|
|
21414
|
+
onFlip?.(currentLeft + 1, targetSpread + 1);
|
|
21387
21415
|
if (dir === 1) {
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
this._drawPage(ctx, this._pages[sp + 2] || null, bx, by, pageW, pageH);
|
|
21391
|
-
this._drawPage(ctx, this._pages[sp + 3] || null, bx + pageW, by, pageW, pageH);
|
|
21392
|
-
if (foldX > bx + pageW) {
|
|
21393
|
-
ctx.save();
|
|
21394
|
-
ctx.beginPath();
|
|
21395
|
-
ctx.rect(bx + pageW, by, foldX - (bx + pageW), pageH);
|
|
21396
|
-
ctx.clip();
|
|
21397
|
-
this._drawPage(ctx, this._pages[sp + 1] || null, bx + pageW, by, pageW, pageH);
|
|
21398
|
-
ctx.restore();
|
|
21399
|
-
}
|
|
21400
|
-
if (turnW > 1) {
|
|
21401
|
-
ctx.save();
|
|
21402
|
-
ctx.beginPath();
|
|
21403
|
-
ctx.rect(foldX, by, turnW, pageH);
|
|
21404
|
-
ctx.clip();
|
|
21405
|
-
const sc = Math.max(0.82, 1 - t * 0.18);
|
|
21406
|
-
ctx.translate(foldX + turnW, by);
|
|
21407
|
-
ctx.scale(-sc, 1);
|
|
21408
|
-
ctx.drawImage(this._pages[sp + 2] || this._pages[sp + 1], 0, 0, turnW / sc, pageH);
|
|
21409
|
-
const lg = ctx.createLinearGradient(0, 0, turnW / sc, 0);
|
|
21410
|
-
lg.addColorStop(0, `rgba(255,255,255,${0.08 * (1 - t)})`);
|
|
21411
|
-
lg.addColorStop(1, `rgba(0,0,0,${0.12 * t})`);
|
|
21412
|
-
ctx.fillStyle = lg;
|
|
21413
|
-
ctx.fillRect(0, 0, turnW / sc, pageH);
|
|
21414
|
-
ctx.restore();
|
|
21415
|
-
}
|
|
21416
|
-
const fg = ctx.createLinearGradient(foldX - 28, 0, foldX + 8, 0);
|
|
21417
|
-
fg.addColorStop(0, "rgba(0,0,0,0)");
|
|
21418
|
-
fg.addColorStop(0.55, `rgba(0,0,0,${si * 0.55})`);
|
|
21419
|
-
fg.addColorStop(0.85, `rgba(0,0,0,${si})`);
|
|
21420
|
-
fg.addColorStop(1, "rgba(255,255,255,0.15)");
|
|
21421
|
-
ctx.fillStyle = fg;
|
|
21422
|
-
ctx.fillRect(foldX - 28, by, 36, pageH);
|
|
21423
|
-
const cg = ctx.createLinearGradient(bx + pageW, 0, bx + pageW - 50, 0);
|
|
21424
|
-
cg.addColorStop(0, `rgba(0,0,0,${si * 0.4 * (1 - t)})`);
|
|
21425
|
-
cg.addColorStop(1, "rgba(0,0,0,0)");
|
|
21426
|
-
ctx.fillStyle = cg;
|
|
21427
|
-
ctx.fillRect(bx + pageW - 50, by, 50, pageH);
|
|
21416
|
+
this._setFace(this._rightBack, targetSpread + 1);
|
|
21417
|
+
this._setFace(this._leftBack, targetSpread);
|
|
21428
21418
|
} else {
|
|
21429
|
-
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
|
|
21434
|
-
|
|
21435
|
-
|
|
21436
|
-
|
|
21437
|
-
|
|
21438
|
-
this._drawPage(ctx, this._pages[sp] || null, bx, by, pageW, pageH);
|
|
21439
|
-
ctx.restore();
|
|
21440
|
-
}
|
|
21441
|
-
if (turnW > 1) {
|
|
21442
|
-
ctx.save();
|
|
21443
|
-
ctx.beginPath();
|
|
21444
|
-
ctx.rect(bx, by, turnW, pageH);
|
|
21445
|
-
ctx.clip();
|
|
21446
|
-
const sc = Math.max(0.82, 1 - t * 0.18);
|
|
21447
|
-
ctx.translate(bx + turnW, by);
|
|
21448
|
-
ctx.scale(-sc, 1);
|
|
21449
|
-
ctx.drawImage(this._pages[sp - 2] || this._pages[sp], 0, 0, turnW / sc, pageH);
|
|
21450
|
-
const lg = ctx.createLinearGradient(turnW / sc, 0, 0, 0);
|
|
21451
|
-
lg.addColorStop(0, `rgba(255,255,255,${0.08 * (1 - t)})`);
|
|
21452
|
-
lg.addColorStop(1, `rgba(0,0,0,${0.12 * t})`);
|
|
21453
|
-
ctx.fillStyle = lg;
|
|
21454
|
-
ctx.fillRect(0, 0, turnW / sc, pageH);
|
|
21455
|
-
ctx.restore();
|
|
21456
|
-
}
|
|
21457
|
-
const fg = ctx.createLinearGradient(foldX - 8, 0, foldX + 28, 0);
|
|
21458
|
-
fg.addColorStop(0, "rgba(255,255,255,0.15)");
|
|
21459
|
-
fg.addColorStop(0.15, `rgba(0,0,0,${si})`);
|
|
21460
|
-
fg.addColorStop(0.45, `rgba(0,0,0,${si * 0.55})`);
|
|
21461
|
-
fg.addColorStop(1, "rgba(0,0,0,0)");
|
|
21462
|
-
ctx.fillStyle = fg;
|
|
21463
|
-
ctx.fillRect(foldX - 8, by, 36, pageH);
|
|
21464
|
-
const cg = ctx.createLinearGradient(bx + pageW, 0, bx + pageW + 50, 0);
|
|
21465
|
-
cg.addColorStop(0, `rgba(0,0,0,${si * 0.4 * (1 - t)})`);
|
|
21466
|
-
cg.addColorStop(1, "rgba(0,0,0,0)");
|
|
21467
|
-
ctx.fillStyle = cg;
|
|
21468
|
-
ctx.fillRect(bx + pageW, by, 50, pageH);
|
|
21469
|
-
}
|
|
21470
|
-
this._drawSpine(ctx, bx + pageW, by, pageH);
|
|
21471
|
-
}
|
|
21472
|
-
_startFlip(dir, targetSpread) {
|
|
21473
|
-
if (this._flipping || !this._total) return;
|
|
21474
|
-
const next = targetSpread !== void 0 ? targetSpread : this._spread + dir * 2;
|
|
21475
|
-
if (next < 0 || next + 1 > this._total) return;
|
|
21476
|
-
this._flipping = true;
|
|
21477
|
-
this._opts.onFlip?.(this._spread + 1, next + 1);
|
|
21478
|
-
const duration = this._opts.flipDuration;
|
|
21479
|
-
let start = null;
|
|
21419
|
+
this._setFace(this._leftBack, targetSpread);
|
|
21420
|
+
this._setFace(this._rightBack, targetSpread + 1);
|
|
21421
|
+
}
|
|
21422
|
+
const flippingPage = dir === 1 ? this._el.querySelector(".pfb__page--right") : this._el.querySelector(".pfb__page--left");
|
|
21423
|
+
const shadow2 = dir === 1 ? this._rightShadow : this._leftShadow;
|
|
21424
|
+
const startAngle = 0;
|
|
21425
|
+
const endAngle = dir === 1 ? -180 : 180;
|
|
21426
|
+
const easeInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
21427
|
+
let startTime = null;
|
|
21480
21428
|
const step = (ts) => {
|
|
21481
|
-
if (!
|
|
21482
|
-
const
|
|
21483
|
-
const
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21429
|
+
if (!startTime) startTime = ts;
|
|
21430
|
+
const elapsed = ts - startTime;
|
|
21431
|
+
const progress = clamp(elapsed / flipDuration, 0, 1);
|
|
21432
|
+
const eased = easeInOut(progress);
|
|
21433
|
+
const angle = startAngle + (endAngle - startAngle) * eased;
|
|
21434
|
+
flippingPage.style.transform = `rotateY(${angle}deg)`;
|
|
21435
|
+
const shadowPeak = Math.sin(progress * Math.PI);
|
|
21436
|
+
shadow2.style.opacity = shadowPeak * shadowIntensity;
|
|
21437
|
+
if (progress < 1) {
|
|
21438
|
+
requestAnimationFrame(step);
|
|
21487
21439
|
} else {
|
|
21488
|
-
|
|
21440
|
+
flippingPage.style.transform = "";
|
|
21441
|
+
shadow2.style.opacity = 0;
|
|
21442
|
+
this._spread = targetSpread;
|
|
21489
21443
|
this._flipping = false;
|
|
21490
|
-
this.
|
|
21491
|
-
this._updateUI();
|
|
21444
|
+
this._renderSpread(targetSpread, false);
|
|
21492
21445
|
}
|
|
21493
21446
|
};
|
|
21494
|
-
|
|
21447
|
+
requestAnimationFrame(step);
|
|
21495
21448
|
}
|
|
21449
|
+
// ── Events ────────────────────────────────────────────────────────────────
|
|
21496
21450
|
_bindEvents() {
|
|
21497
|
-
this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev")
|
|
21498
|
-
this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next")
|
|
21451
|
+
const prevBtns = this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev");
|
|
21452
|
+
const nextBtns = this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next");
|
|
21453
|
+
prevBtns.forEach((b) => b.addEventListener("click", () => this.prev()));
|
|
21454
|
+
nextBtns.forEach((b) => b.addEventListener("click", () => this.next()));
|
|
21499
21455
|
this._onKey = (e) => {
|
|
21500
|
-
if (
|
|
21501
|
-
if (
|
|
21456
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") this.next();
|
|
21457
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowUp") this.prev();
|
|
21502
21458
|
};
|
|
21503
21459
|
window.addEventListener("keydown", this._onKey);
|
|
21504
|
-
this.
|
|
21505
|
-
|
|
21506
|
-
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
tx0 = e.touches[0].clientX;
|
|
21460
|
+
if (isTouchDevice()) this._bindSwipe();
|
|
21461
|
+
this._onResize = () => this._updateUI();
|
|
21462
|
+
window.addEventListener("resize", this._onResize);
|
|
21463
|
+
}
|
|
21464
|
+
_bindSwipe() {
|
|
21465
|
+
let startX = 0;
|
|
21466
|
+
this._stage.addEventListener("touchstart", (e) => {
|
|
21467
|
+
startX = e.touches[0].clientX;
|
|
21513
21468
|
}, { passive: true });
|
|
21514
|
-
this.
|
|
21515
|
-
const dx = e.changedTouches[0].clientX -
|
|
21469
|
+
this._stage.addEventListener("touchend", (e) => {
|
|
21470
|
+
const dx = e.changedTouches[0].clientX - startX;
|
|
21516
21471
|
if (Math.abs(dx) > 40) dx < 0 ? this.next() : this.prev();
|
|
21517
21472
|
}, { passive: true });
|
|
21518
|
-
this._onResize = () => this._resizeCanvas();
|
|
21519
|
-
window.addEventListener("resize", this._onResize);
|
|
21520
21473
|
}
|
|
21474
|
+
// ── UI Helpers ────────────────────────────────────────────────────────────
|
|
21521
21475
|
_updateUI() {
|
|
21522
|
-
this.
|
|
21523
|
-
|
|
21476
|
+
if (!this._pageInfo) return;
|
|
21477
|
+
const leftPage = this._spread + 1;
|
|
21478
|
+
const rightPage = this._spread + 2;
|
|
21479
|
+
if (rightPage <= this._total) {
|
|
21480
|
+
this._pageInfo.textContent = `${leftPage} \u2013 ${rightPage} / ${this._total}`;
|
|
21481
|
+
} else {
|
|
21482
|
+
this._pageInfo.textContent = `${leftPage} / ${this._total}`;
|
|
21483
|
+
}
|
|
21484
|
+
const prevBtns = this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev");
|
|
21485
|
+
const nextBtns = this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next");
|
|
21486
|
+
prevBtns.forEach((b) => {
|
|
21487
|
+
b.disabled = this._spread === 0;
|
|
21524
21488
|
});
|
|
21525
|
-
|
|
21489
|
+
nextBtns.forEach((b) => {
|
|
21526
21490
|
b.disabled = this._spread + 2 >= this._total;
|
|
21527
21491
|
});
|
|
21528
|
-
if (this._pageInfo) {
|
|
21529
|
-
const r = this._spread + 2;
|
|
21530
|
-
this._pageInfo.textContent = r <= this._total ? `${this._spread + 1} \u2013 ${r} / ${this._total}` : `${this._spread + 1} / ${this._total}`;
|
|
21531
|
-
}
|
|
21532
21492
|
}
|
|
21533
21493
|
_showError(msg) {
|
|
21534
21494
|
this._loader.hidden = true;
|
|
21535
21495
|
this._errorEl.hidden = false;
|
|
21536
|
-
this._errorEl.textContent = `\u26A0\uFE0F ${msg}`;
|
|
21496
|
+
this._errorEl.textContent = `\u26A0\uFE0F Could not load PDF: ${msg}`;
|
|
21537
21497
|
}
|
|
21538
21498
|
};
|
|
21539
21499
|
function createFlipbook(container, pdfUrl, options) {
|