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