pdf-flipbook 1.3.0 → 1.8.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,291 +21209,406 @@ 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
- /** 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 */
21217
- maxDpr: 2,
21218
- /** Whether to show the toolbar */
21212
+ flippingTime: 800,
21219
21213
  toolbar: true,
21220
- /** Toolbar theme: "light" | "dark" */
21221
21214
  theme: "dark",
21222
- /** Preload N pages ahead */
21223
- preloadAhead: 2,
21224
- /** Called when a page turn starts: (fromPage, toPage) */
21215
+ maxDpr: 2,
21225
21216
  onFlip: null,
21226
- /** Called when all pages are loaded */
21227
21217
  onReady: null,
21228
- /** Called on load error */
21229
21218
  onError: null
21230
21219
  };
21231
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
21232
- var even = (n) => n % 2 === 0 ? n : n - 1;
21233
- var isTouchDevice = () => "ontouchstart" in window || navigator.maxTouchPoints > 0;
21220
+ var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
21221
+ var easeInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
21234
21222
  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
- */
21240
21223
  constructor(container, pdfUrl, options = {}) {
21241
21224
  if (!pdfUrl) throw new Error("[PdfFlipbook] pdfUrl is required");
21242
21225
  this._el = typeof container === "string" ? document.querySelector(container) : container;
21243
- if (!this._el) throw new Error(`[PdfFlipbook] container not found: ${container}`);
21226
+ if (!this._el) throw new Error("[PdfFlipbook] container not found");
21244
21227
  this._url = pdfUrl;
21245
21228
  this._opts = { ...DEFAULTS, ...options };
21246
21229
  this._pages = [];
21247
- this._pdf = null;
21248
21230
  this._total = 0;
21249
21231
  this._spread = 0;
21250
- this._flipping = false;
21232
+ this._flip = null;
21233
+ this._raf = null;
21234
+ this._aspect = 1.414;
21251
21235
  this._init();
21252
21236
  }
21253
- // ── Static workerSrc override ──────────────────────────────────────────────
21254
21237
  static set workerSrc(src) {
21255
21238
  __webpack_exports__GlobalWorkerOptions.workerSrc = src;
21256
21239
  }
21257
- // ═══ PUBLIC API ═══════════════════════════════════════════════════════════
21258
- /** Go to the next spread */
21259
21240
  next() {
21260
- this._flip(1);
21241
+ if (!this._flip) this._startFlip(1);
21261
21242
  }
21262
- /** Go to the previous spread */
21263
21243
  prev() {
21264
- this._flip(-1);
21244
+ if (!this._flip) this._startFlip(-1);
21265
21245
  }
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);
21246
+ goTo(n) {
21247
+ const idx = clamp(n - 1, 0, this._total - 1);
21248
+ const s = idx % 2 === 0 ? idx : idx - 1;
21249
+ if (s !== this._spread && !this._flip) this._startFlip(s > this._spread ? 1 : -1, s);
21273
21250
  }
21274
- /** Destroy the flipbook and clean up */
21275
21251
  destroy() {
21276
- this._el.innerHTML = "";
21277
- this._el.classList.remove("pfb", `pfb--${this._opts.theme}`);
21278
- this._pages = [];
21279
- window.removeEventListener("resize", this._onResize);
21252
+ cancelAnimationFrame(this._raf);
21280
21253
  window.removeEventListener("keydown", this._onKey);
21254
+ window.removeEventListener("resize", this._onResize);
21255
+ this._el.innerHTML = "";
21281
21256
  }
21282
- // ═══ PRIVATE ══════════════════════════════════════════════════════════════
21283
21257
  async _init() {
21284
21258
  this._buildShell();
21285
21259
  try {
21286
- await this._loadPdf();
21260
+ await this._load();
21287
21261
  this._bindEvents();
21262
+ this._raf = requestAnimationFrame(() => this._loop());
21288
21263
  this._opts.onReady?.();
21289
- } catch (err) {
21290
- console.error("[PdfFlipbook]", err);
21291
- this._showError(err.message);
21292
- this._opts.onError?.(err);
21264
+ } catch (e) {
21265
+ console.error("[PdfFlipbook]", e);
21266
+ this._showError(e.message);
21267
+ this._opts.onError?.(e);
21293
21268
  }
21294
21269
  }
21295
- // ── DOM Shell ──────────────────────────────────────────────────────────────
21296
21270
  _buildShell() {
21297
21271
  this._el.classList.add("pfb", `pfb--${this._opts.theme}`);
21298
21272
  this._el.innerHTML = `
21299
21273
  <div class="pfb__stage">
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>
21274
+ <canvas class="pfb__canvas"></canvas>
21275
+ <button class="pfb__btn pfb__btn--prev">&#8592;</button>
21276
+ <button class="pfb__btn pfb__btn--next">&#8594;</button>
21277
+ <button class="pfb__btn--fs" aria-label="Fullscreen">&#x26F6;</button>
21278
+ <div class="pfb__loader"><div class="pfb__spinner"></div><span class="pfb__loader-text">Loading\u2026</span></div>
21319
21279
  <div class="pfb__error" hidden></div>
21320
21280
  </div>
21321
- ${this._opts.toolbar ? `
21322
- <div class="pfb__toolbar">
21323
- <button class="pfb__tb-btn pfb__tb-prev" aria-label="Previous">&#8592;</button>
21281
+ ${this._opts.toolbar ? `<div class="pfb__toolbar">
21282
+ <button class="pfb__tb-btn pfb__tb-prev">&#8592;</button>
21324
21283
  <span class="pfb__page-info"></span>
21325
- <button class="pfb__tb-btn pfb__tb-next" aria-label="Next">&#8594;</button>
21326
- </div>` : ""}
21327
- `;
21284
+ <button class="pfb__tb-btn pfb__tb-next">&#8594;</button>
21285
+ </div>` : ""}`;
21328
21286
  this._stage = this._el.querySelector(".pfb__stage");
21329
- this._book = this._el.querySelector(".pfb__book");
21287
+ this._canvas = this._el.querySelector(".pfb__canvas");
21288
+ this._ctx = this._canvas.getContext("2d");
21330
21289
  this._loader = this._el.querySelector(".pfb__loader");
21290
+ this._loaderT = this._el.querySelector(".pfb__loader-text");
21331
21291
  this._errorEl = this._el.querySelector(".pfb__error");
21332
- this._pageInfo = this._el.querySelector(".pfb__page-info");
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 ────────────────────────────────────────────────────────────
21341
- async _loadPdf() {
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();
21346
- this._loader.hidden = true;
21347
- this._renderSpread(0, false);
21348
- }
21349
- async _prerenderAll() {
21350
- this._loaderText = this._el.querySelector(".pfb__loader-text");
21292
+ this._pgInfo = this._el.querySelector(".pfb__page-info");
21293
+ this._resizeCanvas();
21294
+ }
21295
+ _resizeCanvas() {
21296
+ const w = this._stage.clientWidth || 900;
21297
+ const h = this._stage.clientHeight || 560;
21298
+ this._canvas.width = w;
21299
+ this._canvas.height = h;
21300
+ this._canvas.style.width = w + "px";
21301
+ this._canvas.style.height = h + "px";
21302
+ }
21303
+ async _load() {
21304
+ const pdf = await __webpack_exports__getDocument({ url: this._url, withCredentials: false }).promise;
21305
+ this._total = pdf.numPages;
21306
+ const fp = await pdf.getPage(1);
21307
+ const vp0 = fp.getViewport({ scale: 1 });
21308
+ this._aspect = vp0.height / vp0.width;
21351
21309
  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);
21310
+ if (this._loaderT) this._loaderT.textContent = `Loading ${i} / ${this._total}\u2026`;
21311
+ const page = await pdf.getPage(i);
21312
+ const dpr = clamp(window.devicePixelRatio || 1, 1, this._opts.maxDpr);
21313
+ const W = 1200 * dpr;
21314
+ const vp = page.getViewport({ scale: W / vp0.width });
21315
+ const cvs = document.createElement("canvas");
21316
+ cvs.width = vp.width;
21317
+ cvs.height = vp.height;
21318
+ await page.render({ canvasContext: cvs.getContext("2d"), viewport: vp }).promise;
21319
+ this._pages.push(cvs);
21320
+ }
21321
+ this._loader.hidden = true;
21385
21322
  this._updateUI();
21386
21323
  }
21387
- _setFace(face, pageIndex) {
21388
- face.innerHTML = "";
21389
- if (pageIndex === null || pageIndex >= this._total) {
21390
- face.classList.add("pfb__face--blank");
21324
+ // ── Layout ────────────────────────────────────────────────────────────────
21325
+ _bookLayout() {
21326
+ const cW = this._canvas.width;
21327
+ const cH = this._canvas.height;
21328
+ const mob = cW < 560;
21329
+ const arrowPad = mob ? 48 : 64;
21330
+ const maxW = cW - arrowPad * 2;
21331
+ let pH = cH - 8;
21332
+ let pW = Math.floor(pH / this._aspect);
21333
+ if (!mob && pW * 2 > maxW) {
21334
+ pW = Math.floor(maxW / 2);
21335
+ pH = Math.floor(pW * this._aspect);
21336
+ }
21337
+ if (mob && pW > maxW) {
21338
+ pW = maxW;
21339
+ pH = Math.floor(pW * this._aspect);
21340
+ }
21341
+ const bx = Math.floor((cW - (mob ? pW : pW * 2)) / 2);
21342
+ const by = Math.floor((cH - pH) / 2);
21343
+ return { bx, by, pW, pH, mob };
21344
+ }
21345
+ // ── Render loop ───────────────────────────────────────────────────────────
21346
+ _loop() {
21347
+ this._draw();
21348
+ this._raf = requestAnimationFrame(() => this._loop());
21349
+ }
21350
+ _draw() {
21351
+ const ctx = this._ctx;
21352
+ ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
21353
+ if (!this._total) return;
21354
+ const { bx, by, pW, pH, mob } = this._bookLayout();
21355
+ if (mob) {
21356
+ this._blitPage(ctx, this._pages[this._spread] || null, bx, by, pW, pH);
21391
21357
  return;
21392
21358
  }
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);
21399
- }
21400
- }
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);
21359
+ if (!this._flip) {
21360
+ this._drawSpread(ctx, this._spread, bx, by, pW, pH);
21361
+ } else {
21362
+ this._drawFlip(ctx, bx, by, pW, pH);
21363
+ }
21364
+ }
21365
+ // ── Draw a static spread ──────────────────────────────────────────────────
21366
+ _drawSpread(ctx, spread, bx, by, pW, pH) {
21367
+ this._blitPage(ctx, this._pages[spread] || null, bx, by, pW, pH);
21368
+ this._blitPage(ctx, this._pages[spread + 1] || null, bx + pW, by, pW, pH);
21369
+ this._drawSpineAndShadow(ctx, bx, by, pW, pH);
21370
+ }
21371
+ _blitPage(ctx, cvs, x, y, w, h) {
21372
+ ctx.fillStyle = "#ffffff";
21373
+ ctx.fillRect(x, y, w, h);
21374
+ if (cvs) ctx.drawImage(cvs, x, y, w, h);
21375
+ }
21376
+ _drawSpineAndShadow(ctx, bx, by, pW, pH) {
21377
+ const sx = bx + pW;
21378
+ const sg = ctx.createLinearGradient(sx - 18, 0, sx + 18, 0);
21379
+ sg.addColorStop(0, "rgba(0,0,0,0)");
21380
+ sg.addColorStop(0.35, "rgba(0,0,0,0.12)");
21381
+ sg.addColorStop(0.5, "rgba(0,0,0,0.18)");
21382
+ sg.addColorStop(0.65, "rgba(0,0,0,0.12)");
21383
+ sg.addColorStop(1, "rgba(0,0,0,0)");
21384
+ ctx.fillStyle = sg;
21385
+ ctx.fillRect(sx - 18, by, 36, pH);
21386
+ const dg = ctx.createLinearGradient(0, by + pH - 4, 0, by + pH + 38);
21387
+ dg.addColorStop(0, "rgba(0,0,0,0.38)");
21388
+ dg.addColorStop(1, "rgba(0,0,0,0)");
21389
+ ctx.fillStyle = dg;
21390
+ ctx.fillRect(bx - 16, by + pH - 4, pW * 2 + 32, 42);
21391
+ }
21392
+ // ── Flip render ───────────────────────────────────────────────────────────
21393
+ _drawFlip(ctx, bx, by, pW, pH) {
21394
+ const f = this._flip;
21395
+ const t = easeInOut(f.progress);
21396
+ const dir = f.dir;
21397
+ const srcSpread = f.src;
21398
+ const destSpread = f.dest;
21399
+ const srcL = this._pages[srcSpread] || null;
21400
+ const srcR = this._pages[srcSpread + 1] || null;
21401
+ const dstL = this._pages[destSpread] || null;
21402
+ const dstR = this._pages[destSpread + 1] || null;
21403
+ const spine = bx + pW;
21415
21404
  if (dir === 1) {
21416
- this._setFace(this._rightBack, targetSpread + 1);
21417
- this._setFace(this._leftBack, targetSpread);
21405
+ this._blitPage(ctx, dstR, spine, by, pW, pH);
21406
+ this._blitPage(ctx, srcL, bx, by, pW, pH);
21407
+ if (t <= 0.5) {
21408
+ const frontT = 1 - t * 2;
21409
+ const frontW = Math.floor(pW * frontT);
21410
+ if (frontW > 0) {
21411
+ ctx.save();
21412
+ ctx.beginPath();
21413
+ ctx.rect(spine, by, frontW, pH);
21414
+ ctx.clip();
21415
+ this._blitPage(ctx, srcR, spine, by, pW, pH);
21416
+ ctx.restore();
21417
+ }
21418
+ const shadowW = Math.min(80, pW * 0.3) * (1 - frontT);
21419
+ if (shadowW > 0) {
21420
+ const shadowX = spine - shadowW;
21421
+ const sg = ctx.createLinearGradient(spine, 0, shadowX, 0);
21422
+ sg.addColorStop(0, "rgba(0,0,0,0.38)");
21423
+ sg.addColorStop(0.5, "rgba(0,0,0,0.12)");
21424
+ sg.addColorStop(1, "rgba(0,0,0,0)");
21425
+ ctx.fillStyle = sg;
21426
+ ctx.fillRect(shadowX, by, shadowW, pH);
21427
+ }
21428
+ const cx = spine + frontW;
21429
+ this._drawCrease(ctx, cx, by, pH, "right");
21430
+ } else {
21431
+ const backT = (t - 0.5) * 2;
21432
+ const backW = Math.floor(pW * backT);
21433
+ if (backW > 0) {
21434
+ const backX = spine - backW;
21435
+ ctx.save();
21436
+ ctx.beginPath();
21437
+ ctx.rect(backX, by, backW, pH);
21438
+ ctx.clip();
21439
+ ctx.save();
21440
+ ctx.translate(spine, by);
21441
+ ctx.scale(-1, 1);
21442
+ this._blitPage(ctx, dstL, 0, 0, pW, pH);
21443
+ ctx.fillStyle = `rgba(0,0,0,${0.08 + 0.12 * backT})`;
21444
+ ctx.fillRect(0, 0, pW, pH);
21445
+ ctx.restore();
21446
+ ctx.restore();
21447
+ }
21448
+ const shadowW = Math.min(80, pW * 0.3) * (1 - backT);
21449
+ if (shadowW > 0) {
21450
+ const shadowX = spine - backW - shadowW;
21451
+ const sg = ctx.createLinearGradient(spine - backW, 0, shadowX, 0);
21452
+ sg.addColorStop(0, "rgba(0,0,0,0.3)");
21453
+ sg.addColorStop(1, "rgba(0,0,0,0)");
21454
+ ctx.fillStyle = sg;
21455
+ ctx.fillRect(Math.max(bx, shadowX), by, Math.min(shadowW, spine - backW - bx), pH);
21456
+ }
21457
+ const cx = spine - backW;
21458
+ this._drawCrease(ctx, cx, by, pH, "left");
21459
+ }
21460
+ } else {
21461
+ this._blitPage(ctx, dstL, bx, by, pW, pH);
21462
+ this._blitPage(ctx, srcR, spine, by, pW, pH);
21463
+ if (t <= 0.5) {
21464
+ const frontT = 1 - t * 2;
21465
+ const frontW = Math.floor(pW * frontT);
21466
+ const frontX = spine - frontW;
21467
+ if (frontW > 0) {
21468
+ ctx.save();
21469
+ ctx.beginPath();
21470
+ ctx.rect(frontX, by, frontW, pH);
21471
+ ctx.clip();
21472
+ this._blitPage(ctx, srcL, bx, by, pW, pH);
21473
+ ctx.restore();
21474
+ }
21475
+ const shadowW = Math.min(80, pW * 0.3) * (1 - frontT);
21476
+ if (shadowW > 0) {
21477
+ const sg = ctx.createLinearGradient(spine, 0, spine + shadowW, 0);
21478
+ sg.addColorStop(0, "rgba(0,0,0,0.38)");
21479
+ sg.addColorStop(0.5, "rgba(0,0,0,0.12)");
21480
+ sg.addColorStop(1, "rgba(0,0,0,0)");
21481
+ ctx.fillStyle = sg;
21482
+ ctx.fillRect(spine, by, shadowW, pH);
21483
+ }
21484
+ const cx = spine - frontW;
21485
+ this._drawCrease(ctx, cx, by, pH, "left");
21486
+ } else {
21487
+ const backT = (t - 0.5) * 2;
21488
+ const backW = Math.floor(pW * backT);
21489
+ const backX = spine;
21490
+ if (backW > 0) {
21491
+ ctx.save();
21492
+ ctx.beginPath();
21493
+ ctx.rect(backX, by, backW, pH);
21494
+ ctx.clip();
21495
+ ctx.save();
21496
+ ctx.translate(spine, by);
21497
+ ctx.scale(-1, 1);
21498
+ ctx.translate(-pW, 0);
21499
+ this._blitPage(ctx, dstR, 0, 0, pW, pH);
21500
+ ctx.fillStyle = `rgba(0,0,0,${0.08 + 0.12 * backT})`;
21501
+ ctx.fillRect(0, 0, pW, pH);
21502
+ ctx.restore();
21503
+ ctx.restore();
21504
+ }
21505
+ const shadowW = Math.min(80, pW * 0.3) * (1 - backT);
21506
+ if (shadowW > 0) {
21507
+ const shadowEdge = spine + backW + shadowW;
21508
+ const sg = ctx.createLinearGradient(spine + backW, 0, shadowEdge, 0);
21509
+ sg.addColorStop(0, "rgba(0,0,0,0.3)");
21510
+ sg.addColorStop(1, "rgba(0,0,0,0)");
21511
+ ctx.fillStyle = sg;
21512
+ ctx.fillRect(spine + backW, by, Math.min(shadowW, bx + pW * 2 - spine - backW), pH);
21513
+ }
21514
+ const cx = spine + backW;
21515
+ this._drawCrease(ctx, cx, by, pH, "right");
21516
+ }
21517
+ }
21518
+ this._drawSpineAndShadow(ctx, bx, by, pW, pH);
21519
+ }
21520
+ _drawCrease(ctx, cx, by, pH, highlightSide) {
21521
+ const w = 10;
21522
+ const g = ctx.createLinearGradient(cx - w, 0, cx + w, 0);
21523
+ if (highlightSide === "right") {
21524
+ g.addColorStop(0, "rgba(0,0,0,0)");
21525
+ g.addColorStop(0.35, "rgba(0,0,0,0.28)");
21526
+ g.addColorStop(0.55, "rgba(255,255,255,0.22)");
21527
+ g.addColorStop(1, "rgba(0,0,0,0)");
21418
21528
  } else {
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;
21529
+ g.addColorStop(0, "rgba(0,0,0,0)");
21530
+ g.addColorStop(0.45, "rgba(255,255,255,0.22)");
21531
+ g.addColorStop(0.65, "rgba(0,0,0,0.28)");
21532
+ g.addColorStop(1, "rgba(0,0,0,0)");
21533
+ }
21534
+ ctx.fillStyle = g;
21535
+ ctx.fillRect(cx - w, by, w * 2, pH);
21536
+ }
21537
+ // ── Flip state ────────────────────────────────────────────────────────────
21538
+ _startFlip(dir, dest) {
21539
+ if (this._flip || !this._total) return;
21540
+ const target = dest !== void 0 ? dest : this._spread + dir * 2;
21541
+ if (target < 0 || target + 1 > this._total) return;
21542
+ this._opts.onFlip?.(this._spread + 1, target + 1);
21543
+ this._flip = { dir, src: this._spread, dest: target, progress: 0, start: null };
21544
+ const duration = this._opts.flippingTime;
21428
21545
  const step = (ts) => {
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);
21546
+ if (!this._flip) return;
21547
+ if (!this._flip.start) this._flip.start = ts;
21548
+ this._flip.progress = clamp((ts - this._flip.start) / duration, 0, 1);
21549
+ if (this._flip.progress >= 1) {
21550
+ this._spread = target;
21551
+ this._flip = null;
21552
+ this._updateUI();
21439
21553
  } else {
21440
- flippingPage.style.transform = "";
21441
- shadow2.style.opacity = 0;
21442
- this._spread = targetSpread;
21443
- this._flipping = false;
21444
- this._renderSpread(targetSpread, false);
21554
+ requestAnimationFrame(step);
21445
21555
  }
21446
21556
  };
21447
21557
  requestAnimationFrame(step);
21448
21558
  }
21449
21559
  // ── Events ────────────────────────────────────────────────────────────────
21450
21560
  _bindEvents() {
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()));
21561
+ this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev").forEach((b) => b.addEventListener("click", () => this.prev()));
21562
+ this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next").forEach((b) => b.addEventListener("click", () => this.next()));
21563
+ const fsBtn = this._el.querySelector(".pfb__btn--fs");
21564
+ if (fsBtn) {
21565
+ fsBtn.addEventListener("click", () => {
21566
+ if (!document.fullscreenElement) {
21567
+ this._el.requestFullscreen?.() || this._el.webkitRequestFullscreen?.();
21568
+ } else {
21569
+ document.exitFullscreen?.() || document.webkitExitFullscreen?.();
21570
+ }
21571
+ });
21572
+ }
21455
21573
  this._onKey = (e) => {
21456
- if (e.key === "ArrowRight" || e.key === "ArrowDown") this.next();
21457
- if (e.key === "ArrowLeft" || e.key === "ArrowUp") this.prev();
21574
+ if (["ArrowRight", "ArrowDown"].includes(e.key)) this.next();
21575
+ if (["ArrowLeft", "ArrowUp"].includes(e.key)) this.prev();
21458
21576
  };
21459
21577
  window.addEventListener("keydown", this._onKey);
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;
21578
+ this._canvas.addEventListener("click", (e) => {
21579
+ if (this._flip) return;
21580
+ const rect = this._canvas.getBoundingClientRect();
21581
+ if (e.clientX - rect.left < this._canvas.width / 2) this.prev();
21582
+ else this.next();
21583
+ });
21584
+ let tx = 0;
21585
+ this._canvas.addEventListener("touchstart", (e) => {
21586
+ tx = e.touches[0].clientX;
21468
21587
  }, { passive: true });
21469
- this._stage.addEventListener("touchend", (e) => {
21470
- const dx = e.changedTouches[0].clientX - startX;
21588
+ this._canvas.addEventListener("touchend", (e) => {
21589
+ const dx = e.changedTouches[0].clientX - tx;
21471
21590
  if (Math.abs(dx) > 40) dx < 0 ? this.next() : this.prev();
21472
21591
  }, { passive: true });
21592
+ this._onResize = () => this._resizeCanvas();
21593
+ window.addEventListener("resize", this._onResize);
21473
21594
  }
21474
- // ── UI Helpers ────────────────────────────────────────────────────────────
21475
21595
  _updateUI() {
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}`;
21596
+ const i = this._spread;
21597
+ const r = i + 2;
21598
+ if (this._pgInfo) {
21599
+ this._pgInfo.textContent = r <= this._total ? `${i + 1} \u2013 ${r} / ${this._total}` : `${i + 1} / ${this._total}`;
21483
21600
  }
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;
21601
+ this._el.querySelectorAll(".pfb__btn--prev, .pfb__tb-prev").forEach((b) => {
21602
+ b.disabled = i <= 0;
21488
21603
  });
21489
- nextBtns.forEach((b) => {
21490
- b.disabled = this._spread + 2 >= this._total;
21604
+ this._el.querySelectorAll(".pfb__btn--next, .pfb__tb-next").forEach((b) => {
21605
+ b.disabled = i + 2 >= this._total;
21491
21606
  });
21492
21607
  }
21493
21608
  _showError(msg) {
21494
21609
  this._loader.hidden = true;
21495
21610
  this._errorEl.hidden = false;
21496
- this._errorEl.textContent = `\u26A0\uFE0F Could not load PDF: ${msg}`;
21611
+ this._errorEl.textContent = `\u26A0\uFE0F ${msg}`;
21497
21612
  }
21498
21613
  };
21499
21614
  function createFlipbook(container, pdfUrl, options) {