pdf-flipbook 1.2.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.
@@ -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
- flipDuration: 800,
21213
- shadowIntensity: 0.35,
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 lerp = (a, b, t) => a + (b - a) * t;
21223
- var easeInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
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("[PdfFlipbook] container not found");
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._spread = 0;
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._startFlip(1);
21260
+ this._flip(1);
21244
21261
  }
21262
+ /** Go to the previous spread */
21245
21263
  prev() {
21246
- this._startFlip(-1);
21264
+ this._flip(-1);
21247
21265
  }
21248
- goTo(n) {
21249
- const idx = clamp(n - 1, 0, this._total - 1);
21250
- const target = idx % 2 === 0 ? idx : idx - 1;
21251
- if (target !== this._spread) this._startFlip(target > this._spread ? 1 : -1, target);
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
- cancelAnimationFrame(this._animId);
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 (e) {
21266
- console.error("[PdfFlipbook]", e);
21267
- this._showError(e.message);
21268
- this._opts.onError?.(e);
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
- <canvas class="pfb__canvas"></canvas>
21276
- <button class="pfb__btn pfb__btn--prev" aria-label="Previous">&#8592;</button>
21277
- <button class="pfb__btn pfb__btn--next" aria-label="Next">&#8594;</button>
21278
- <div class="pfb__loader"><div class="pfb__spinner"></div><span class="pfb__loader-text">Loading\u2026</span></div>
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">&#8592;</button>
21314
+ <button class="pfb__btn pfb__btn--next" aria-label="Next page">&#8594;</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 ? `<div class="pfb__toolbar">
21282
- <button class="pfb__tb-btn pfb__tb-prev">&#8592;</button>
21321
+ ${this._opts.toolbar ? `
21322
+ <div class="pfb__toolbar">
21323
+ <button class="pfb__tb-btn pfb__tb-prev" aria-label="Previous">&#8592;</button>
21283
21324
  <span class="pfb__page-info"></span>
21284
- <button class="pfb__tb-btn pfb__tb-next">&#8594;</button>
21325
+ <button class="pfb__tb-btn pfb__tb-next" aria-label="Next">&#8594;</button>
21285
21326
  </div>` : ""}
21286
21327
  `;
21287
21328
  this._stage = this._el.querySelector(".pfb__stage");
21288
- this._canvas = this._el.querySelector(".pfb__canvas");
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._resizeCanvas();
21295
- }
21296
- _resizeCanvas() {
21297
- const W = this._stage.clientWidth || 900;
21298
- const H = this._stage.clientHeight || 560;
21299
- this._canvas.width = W;
21300
- this._canvas.height = H;
21301
- this._canvas.style.width = W + "px";
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 pdf = await __webpack_exports__getDocument({ url: this._url, withCredentials: false }).promise;
21307
- this._total = pdf.numPages;
21308
- const fp = await pdf.getPage(1);
21309
- const vp0 = fp.getViewport({ scale: 1 });
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._drawSpread();
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
- _bookRect() {
21328
- const cW = this._canvas.width, cH = this._canvas.height;
21329
- const mobile = cW < 580;
21330
- let bW, bH;
21331
- if (mobile) {
21332
- bW = Math.min(cW - 24, 440);
21333
- bH = bW * this._pageAspect;
21334
- } else {
21335
- bH = Math.min(cH - 60, 680);
21336
- bW = bH / this._pageAspect * 2;
21337
- if (bW > cW - 80) {
21338
- bW = cW - 80;
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
- _renderFlip(ctx, { t, dir }, bx, by, pageW, pageH) {
21385
- const si = this._opts.shadowIntensity;
21386
- const sp = this._spread;
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
- const foldX = lerp(bx + pageW * 2, bx + pageW, t);
21389
- const turnW = bx + pageW * 2 - foldX;
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
- const foldX = lerp(bx, bx + pageW, t);
21430
- const turnW = foldX - bx;
21431
- this._drawPage(ctx, this._pages[sp - 2] || null, bx, by, pageW, pageH);
21432
- this._drawPage(ctx, this._pages[sp - 1] || null, bx + pageW, by, pageW, pageH);
21433
- if (foldX < bx + pageW) {
21434
- ctx.save();
21435
- ctx.beginPath();
21436
- ctx.rect(foldX, by, bx + pageW - foldX, pageH);
21437
- ctx.clip();
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 (!start) start = ts;
21482
- const raw = clamp((ts - start) / duration, 0, 1);
21483
- const t = easeInOut(raw);
21484
- this._drawSpread({ t, dir });
21485
- if (raw < 1) {
21486
- this._animId = requestAnimationFrame(step);
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
- this._spread = next;
21440
+ flippingPage.style.transform = "";
21441
+ shadow2.style.opacity = 0;
21442
+ this._spread = targetSpread;
21489
21443
  this._flipping = false;
21490
- this._drawSpread();
21491
- this._updateUI();
21444
+ this._renderSpread(targetSpread, false);
21492
21445
  }
21493
21446
  };
21494
- this._animId = requestAnimationFrame(step);
21447
+ requestAnimationFrame(step);
21495
21448
  }
21449
+ // ── Events ────────────────────────────────────────────────────────────────
21496
21450
  _bindEvents() {
21497
- this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev").forEach((b) => b.addEventListener("click", () => this.prev()));
21498
- this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next").forEach((b) => b.addEventListener("click", () => this.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 (["ArrowRight", "ArrowDown"].includes(e.key)) this.next();
21501
- if (["ArrowLeft", "ArrowUp"].includes(e.key)) this.prev();
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._canvas.addEventListener("click", (e) => {
21505
- const rect = this._canvas.getBoundingClientRect();
21506
- const cx = e.clientX - rect.left;
21507
- if (cx < this._canvas.width / 2) this.prev();
21508
- else this.next();
21509
- });
21510
- let tx0 = 0;
21511
- this._canvas.addEventListener("touchstart", (e) => {
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._canvas.addEventListener("touchend", (e) => {
21515
- const dx = e.changedTouches[0].clientX - tx0;
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._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev").forEach((b) => {
21523
- b.disabled = this._spread <= 0;
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
- this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next").forEach((b) => {
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) {